Merge pull request #311 from loki-project/refactor-3
Refactoring Part 2
This commit is contained in:
commit
e34e1f0392
|
@ -28,3 +28,5 @@ DerivedData
|
|||
*.xcuserstate
|
||||
Index/
|
||||
|
||||
# CocoaPods
|
||||
Pods
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
[submodule "ThirdParty/Carthage"]
|
||||
path = ThirdParty/Carthage
|
||||
url = https://github.com/loki-project/session-ios-carthage.git
|
||||
[submodule "Pods"]
|
||||
path = Pods
|
||||
url = https://github.com/loki-project/session-ios-pods.git
|
||||
[submodule "ThirdParty/WebRTC"]
|
||||
path = ThirdParty/WebRTC
|
||||
url = https://github.com/signalapp/signal-webrtc-ios-artifacts
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
2.6.0
|
16
.travis.yml
16
.travis.yml
|
@ -1,16 +0,0 @@
|
|||
language: objective-c
|
||||
cache: cocoapods # pod install somtimes takes >20 minutes, so lets cache this
|
||||
|
||||
osx_image: xcode8.3
|
||||
|
||||
env:
|
||||
-EARLY_START_SIMULATOR=1 # early starting simulator reduces false negatives due to test timeouts
|
||||
|
||||
before_install:
|
||||
- brew update # we may not be running the latest version so always update
|
||||
- brew outdated xctool || brew upgrade xctool # only upgrade if outdated (saves 2 minutes)
|
||||
- bundle
|
||||
- bundle exec pod repo update --silent # log output is too long without --silent
|
||||
|
||||
script: make ci
|
||||
|
212
Gemfile.lock
212
Gemfile.lock
|
@ -1,212 +0,0 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.0)
|
||||
activesupport (4.2.10)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.5.2)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
atomos (0.1.3)
|
||||
babosa (1.0.2)
|
||||
claide (1.0.2)
|
||||
cocoapods (1.5.3)
|
||||
activesupport (>= 4.0.2, < 5)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.5.3)
|
||||
cocoapods-deintegrate (>= 1.0.2, < 2.0)
|
||||
cocoapods-downloader (>= 1.2.0, < 2.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-stats (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.3.0, < 2.0)
|
||||
cocoapods-try (>= 1.1.0, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
fourflusher (~> 2.0.1)
|
||||
gh_inspector (~> 1.0)
|
||||
molinillo (~> 0.6.5)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (~> 1.1)
|
||||
xcodeproj (>= 1.5.7, < 2.0)
|
||||
cocoapods-core (1.5.3)
|
||||
activesupport (>= 4.0.2, < 6)
|
||||
fuzzy_match (~> 2.0.4)
|
||||
nap (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.2)
|
||||
cocoapods-downloader (1.2.2)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.0)
|
||||
cocoapods-stats (1.0.0)
|
||||
cocoapods-trunk (1.3.1)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander-fastlane (4.4.6)
|
||||
highline (~> 1.7.2)
|
||||
concurrent-ruby (1.1.3)
|
||||
declarative (0.0.10)
|
||||
declarative-option (0.1.0)
|
||||
digest-crc (0.4.1)
|
||||
domain_name (0.5.20180417)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.5.0)
|
||||
emoji_regex (0.1.1)
|
||||
escape (0.0.4)
|
||||
excon (0.62.0)
|
||||
faraday (0.15.4)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-cookie_jar (0.0.6)
|
||||
faraday (>= 0.7.4)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday_middleware (0.12.2)
|
||||
faraday (>= 0.7.4, < 1.0)
|
||||
fastimage (2.1.5)
|
||||
fastlane (2.112.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.3, < 3.0.0)
|
||||
babosa (>= 1.0.2, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 2.0.0)
|
||||
colored
|
||||
commander-fastlane (>= 4.4.6, < 5.0.0)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (~> 0.1)
|
||||
excon (>= 0.45.0, < 1.0.0)
|
||||
faraday (~> 0.9)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 0.9)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-api-client (>= 0.21.2, < 0.24.0)
|
||||
google-cloud-storage (>= 1.15.0, < 2.0.0)
|
||||
highline (>= 1.7.2, < 2.0.0)
|
||||
json (< 3.0.0)
|
||||
mini_magick (~> 4.5.1)
|
||||
multi_json
|
||||
multi_xml (~> 0.5)
|
||||
multipart-post (~> 2.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
public_suffix (~> 2.0.0)
|
||||
rubyzip (>= 1.2.2, < 2.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
slack-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-notifier (>= 1.6.2, < 2.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.6.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
fourflusher (2.0.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
google-api-client (0.23.9)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.5, < 0.7.0)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
mime-types (~> 3.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.9)
|
||||
google-cloud-core (1.2.7)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-env (1.0.5)
|
||||
faraday (~> 0.11)
|
||||
google-cloud-storage (1.15.0)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.23)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.6.2)
|
||||
googleauth (0.6.7)
|
||||
faraday (~> 0.12)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.7)
|
||||
highline (1.7.10)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.1.0)
|
||||
jwt (2.1.0)
|
||||
memoist (0.16.0)
|
||||
mime-types (3.2.2)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2018.0812)
|
||||
mini_magick (4.5.1)
|
||||
minitest (5.11.3)
|
||||
molinillo (0.6.6)
|
||||
multi_json (1.13.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.2.6)
|
||||
nap (1.1.0)
|
||||
naturally (2.2.0)
|
||||
netrc (0.11.0)
|
||||
os (1.0.0)
|
||||
plist (3.5.0)
|
||||
public_suffix (2.0.5)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rouge (2.0.7)
|
||||
ruby-macho (1.3.1)
|
||||
rubyzip (1.2.2)
|
||||
security (0.1.3)
|
||||
signet (0.11.0)
|
||||
addressable (~> 2.3)
|
||||
faraday (~> 0.9)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.5)
|
||||
CFPropertyList
|
||||
naturally
|
||||
slack-notifier (2.3.2)
|
||||
terminal-notifier (1.8.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
thread_safe (0.3.6)
|
||||
tty-cursor (0.6.0)
|
||||
tty-screen (0.6.5)
|
||||
tty-spinner (0.9.0)
|
||||
tty-cursor (~> 0.6.0)
|
||||
tzinfo (1.2.5)
|
||||
thread_safe (~> 0.1)
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.5)
|
||||
unicode-display_width (1.4.1)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.7.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.2.6)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.0)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
cocoapods
|
||||
fastlane
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.2
|
|
@ -1,56 +0,0 @@
|
|||
pipeline {
|
||||
agent any
|
||||
|
||||
environment {
|
||||
LANG = "en_US.UTF-8"
|
||||
LANGUAGE = "en_US.UTF-8"
|
||||
LC_ALL = "en_US.UTF-8"
|
||||
PATH = "PATH=$HOME/.rbenv/bin:$HOME/.rbenv/shims:/usr/local/bin/:$PATH"
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('env setup') {
|
||||
steps {
|
||||
script {
|
||||
// CHANGE_ID is set only for pull requests, so it is safe to access the pullRequest global variable
|
||||
if (env.CHANGE_ID) {
|
||||
currentBuild.displayName = "PR #${pullRequest.number}: ${pullRequest.title}"
|
||||
}
|
||||
}
|
||||
sh 'make setup'
|
||||
}
|
||||
}
|
||||
stage('build dependencies') {
|
||||
steps {
|
||||
sh 'make dependencies'
|
||||
}
|
||||
}
|
||||
stage('test') {
|
||||
steps {
|
||||
ansiColor('xterm') {
|
||||
sh 'make test'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
script {
|
||||
// CHANGE_ID is set only for pull requests, so it is safe to access the pullRequest global variable
|
||||
if (env.CHANGE_ID) {
|
||||
def comment = pullRequest.comment("👍 Build PASSED commit: ${pullRequest.head}\nbuild: ${currentBuild.absoluteUrl}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
failure {
|
||||
script {
|
||||
// CHANGE_ID is set only for pull requests, so it is safe to access the pullRequest global variable
|
||||
if (env.CHANGE_ID) {
|
||||
def comment = pullRequest.comment("💥 Build FAILED commit: ${pullRequest.head}\nbuild: ${currentBuild.absoluteUrl}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
import Foundation
|
||||
import SignalServiceKit
|
||||
import Curve25519Kit
|
||||
|
||||
enum LokiTestUtilities {
|
||||
|
||||
public static func setUpMockEnvironment() {
|
||||
// Activate the mock Signal environment
|
||||
ClearCurrentAppContextForTests()
|
||||
SetCurrentAppContext(TestAppContext())
|
||||
MockSSKEnvironment.activate()
|
||||
// Register a mock user
|
||||
let identityManager = OWSIdentityManager.shared()
|
||||
let seed = Randomness.generateRandomBytes(16)!
|
||||
let keyPair = Curve25519.generateKeyPair(fromSeed: seed + seed)
|
||||
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
|
||||
databaseConnection.setObject(keyPair, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
|
||||
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey
|
||||
TSAccountManager.sharedInstance().didRegister()
|
||||
}
|
||||
|
||||
public static func generateKeyPair() -> ECKeyPair {
|
||||
return Curve25519.generateKeyPair()
|
||||
}
|
||||
|
||||
public static func getCurrentUserHexEncodedPublicKey() -> String {
|
||||
return OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
|
||||
}
|
||||
|
||||
public static func generateHexEncodedPublicKey() -> String {
|
||||
return generateKeyPair().hexEncodedPublicKey
|
||||
}
|
||||
|
||||
public static func getDevice(for hexEncodedPublicKey: String) -> DeviceLink.Device? {
|
||||
guard let signature = Data.getSecureRandomData(ofSize: 64) else { return nil }
|
||||
return DeviceLink.Device(hexEncodedPublicKey: hexEncodedPublicKey, signature: signature)
|
||||
}
|
||||
|
||||
public static func createContactThread(for hexEncodedPublicKey: String) -> TSContactThread {
|
||||
return TSContactThread.getOrCreateThread(contactId: hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
public static func createGroupThread(groupType: GroupType) -> TSGroupThread? {
|
||||
let hexEncodedGroupID = Randomness.generateRandomBytes(kGroupIdLength)!.toHexString()
|
||||
let groupID: Data
|
||||
switch groupType {
|
||||
case .closedGroup: groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(hexEncodedGroupID)
|
||||
case .openGroup: groupID = LKGroupUtilities.getEncodedOpenGroupIDAsData(hexEncodedGroupID)
|
||||
case .rssFeed: groupID = LKGroupUtilities.getEncodedRSSFeedIDAsData(hexEncodedGroupID)
|
||||
default: return nil
|
||||
}
|
||||
return TSGroupThread.getOrCreateThread(withGroupId: groupID, groupType: groupType)
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
import PromiseKit
|
||||
@testable import SignalServiceKit
|
||||
import XCTest
|
||||
|
||||
class MultiDeviceProtocolTests : XCTestCase {
|
||||
|
||||
private var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
LokiTestUtilities.setUpMockEnvironment()
|
||||
}
|
||||
|
||||
// MARK: - isSlaveThread
|
||||
|
||||
func test_isSlaveThreadShouldReturnFalseOnGroupThreads() {
|
||||
let allGroupTypes: [GroupType] = [ .closedGroup, .openGroup, .rssFeed ]
|
||||
for groupType in allGroupTypes {
|
||||
guard let groupThread = LokiTestUtilities.createGroupThread(groupType: groupType) else { return XCTFail() }
|
||||
XCTAssertFalse(MultiDeviceProtocol.isSlaveThread(groupThread))
|
||||
}
|
||||
}
|
||||
|
||||
func test_isSlaveThreadShouldReturnTheCorrectValues() {
|
||||
let master = LokiTestUtilities.generateHexEncodedPublicKey()
|
||||
let slave = LokiTestUtilities.generateHexEncodedPublicKey()
|
||||
let other = LokiTestUtilities.generateHexEncodedPublicKey()
|
||||
|
||||
guard let masterDevice = LokiTestUtilities.getDevice(for: master) else { return XCTFail() }
|
||||
guard let slaveDevice = LokiTestUtilities.getDevice(for: slave) else { return XCTFail() }
|
||||
|
||||
let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice)
|
||||
|
||||
storage.dbReadWriteConnection.readWrite { transaction in
|
||||
self.storage.addDeviceLink(deviceLink, in: transaction)
|
||||
}
|
||||
|
||||
let masterThread = LokiTestUtilities.createContactThread(for: master)
|
||||
let slaveThread = LokiTestUtilities.createContactThread(for: slave)
|
||||
let otherThread = LokiTestUtilities.createContactThread(for: other)
|
||||
|
||||
storage.dbReadConnection.read { transaction in
|
||||
XCTAssertNotNil(self.storage.getMasterHexEncodedPublicKey(for: slaveThread.contactIdentifier(), in: transaction))
|
||||
}
|
||||
|
||||
XCTAssertFalse(MultiDeviceProtocol.isSlaveThread(masterThread))
|
||||
XCTAssertTrue(MultiDeviceProtocol.isSlaveThread(slaveThread))
|
||||
XCTAssertFalse(MultiDeviceProtocol.isSlaveThread(otherThread))
|
||||
}
|
||||
|
||||
func test_isSlaveThreadShouldWorkInsideATransaction() {
|
||||
let bob = LokiTestUtilities.generateHexEncodedPublicKey()
|
||||
let thread = LokiTestUtilities.createContactThread(for: bob)
|
||||
storage.dbReadWriteConnection.read { transaction in
|
||||
XCTAssertNoThrow(MultiDeviceProtocol.isSlaveThread(thread))
|
||||
}
|
||||
storage.dbReadWriteConnection.readWrite { transaction in
|
||||
XCTAssertNoThrow(MultiDeviceProtocol.isSlaveThread(thread))
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -1,24 +0,0 @@
|
|||
import CryptoSwift
|
||||
import PromiseKit
|
||||
@testable import SignalServiceKit
|
||||
import XCTest
|
||||
|
||||
class OnionRequestAPITests : XCTestCase {
|
||||
private let maxRetryCount: UInt = 2 // Be a bit more stringent when testing
|
||||
private let testPublicKey = "0501da4723331eb54aaa9a6753a0a59f762103de63f1dc40879cb65a5b5f508814"
|
||||
|
||||
func testOnionRequestSending() {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var error: Error? = nil
|
||||
LokiAPI.useOnionRequests = true
|
||||
let _ = attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) { [testPublicKey = self.testPublicKey] in
|
||||
LokiAPI.getSwarm(for: testPublicKey)
|
||||
}.done(on: LokiAPI.workQueue) { _ in
|
||||
semaphore.signal()
|
||||
}.catch(on: LokiAPI.workQueue) {
|
||||
error = $0; semaphore.signal()
|
||||
}
|
||||
semaphore.wait()
|
||||
XCTAssert(error == nil)
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import PromiseKit
|
||||
@testable import SignalServiceKit
|
||||
import XCTest
|
||||
|
||||
class SessionManagementProtocolTests : XCTestCase {
|
||||
|
||||
// TODO: Add tests
|
||||
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
import PromiseKit
|
||||
@testable import SignalServiceKit
|
||||
import XCTest
|
||||
|
||||
class SyncMessagesProtocolTests : XCTestCase {
|
||||
|
||||
private var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
|
||||
private var messageSender: OWSFakeMessageSender { MockSSKEnvironment.shared.messageSender as! OWSFakeMessageSender }
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
LokiTestUtilities.setUpMockEnvironment()
|
||||
}
|
||||
|
||||
func testContactSyncMessageHandling() {
|
||||
// Let's say Alice and Bob have an ongoing conversation. Alice now links a device. Let's call Alice's master device A1
|
||||
// and her slave device A2, and let's call Bob's device B. When Alice links A2 to A1, A2 needs to somehow establish a
|
||||
// session with B (it already established a session with A1 when the devices were linked). How does it do this?
|
||||
//
|
||||
// As part of the linking process, A2 should've received a contact sync from A1. Upon receiving this contact sync,
|
||||
// A2 should send out AFRs to the subset of the contacts it received from A1 for which it doesn't yet have a session (in
|
||||
// theory this should be all of them).
|
||||
let base64EncodedContactData = "AAAA7QpCMDU0ZmI2M2IxYTU4YjU1YTcwNjMxODkyOWRjNmQxMWM4ZWY3OTAxMTZhNzRjOWFmNTVmYTZhMzZlNjhmMTYzYTMyEhBZMyAoLi4uOGYxNjNhMzIpIgZvcmFuZ2UqaQpCMDU0ZmI2M2IxYTU4YjU1YTcwNjMxODkyOWRjNmQxMWM4ZWY3OTAxMTZhNzRjOWFmNTVmYTZhMzZlNjhmMTYzYTMyEiEFT7Y7Gli1WnBjGJKdxtEcjveQEWp0ya9V+mo25o8WOjIYADIgXAgtAlrJr81tnuWyk8TgJhdsKzz+yIui5mXnbcMyPk1AAAAAAOwKQjA1Nzg4MmQzM2E4OTI1NDdiOTI2NjIyYjk0ZDZjMWNmYjI1ZmY2YTczZmQ4OTZlMWIxNmY1ODI0NzRjZjQ3MDE2YhIQWTQgKC4uLmNmNDcwMTZiKSIFYnJvd24qaQpCMDU3ODgyZDMzYTg5MjU0N2I5MjY2MjJiOTRkNmMxY2ZiMjVmZjZhNzNmZDg5NmUxYjE2ZjU4MjQ3NGNmNDcwMTZiEiEFeILTOoklR7kmYiuU1sHPsl/2pz/YluGxb1gkdM9HAWsYADIgD1QA1ofVIccRhbx8AnbygQYo5iOiyGUMG/sGNP1ENRJAAAAAAPAKQjA1OTUyYTRiNTFjNDJkZWE2OWEwYWNhNWU2OTgxYTQ2MDk0NGI2Yjc0NjdkOWQ5OTliOWU3NjExNzdkYWI1NzIxMxIQWTEgKC4uLmRhYjU3MjEzKSIJYmx1ZV9ncmV5KmkKQjA1OTUyYTRiNTFjNDJkZWE2OWEwYWNhNWU2OTgxYTQ2MDk0NGI2Yjc0NjdkOWQ5OTliOWU3NjExNzdkYWI1NzIxMxIhBZUqS1HELeppoKyl5pgaRglEtrdGfZ2Zm552EXfatXITGAAyIBkyX0S08IAuov6faUvaxYsfJtdpww1G4LF6bG5vG7L+QAA="
|
||||
let contactData = Data(base64Encoded: base64EncodedContactData)!
|
||||
let parser = ContactParser(data: contactData)
|
||||
let hexEncodedPublicKeys = parser.parseHexEncodedPublicKeys()
|
||||
let expectation = self.expectation(description: "Send friend request messages")
|
||||
var messageCount = 0
|
||||
let messageSender = self.messageSender
|
||||
messageSender.sendMessageWasCalledBlock = { sentMessage in
|
||||
messageCount += 1
|
||||
guard sentMessage is FriendRequestMessage else {
|
||||
return XCTFail("Expected a friend request to be sent, but found: \(sentMessage).")
|
||||
}
|
||||
guard messageCount == hexEncodedPublicKeys.count else { return }
|
||||
expectation.fulfill()
|
||||
messageSender.sendMessageWasCalledBlock = nil
|
||||
}
|
||||
storage.dbReadWriteConnection.readWrite { transaction in
|
||||
SyncMessagesProtocol.handleContactSyncMessageData(contactData, using: transaction)
|
||||
}
|
||||
wait(for: [ expectation ], timeout: 1)
|
||||
/* TODO: Re-enable when we've split friend request logic from OWSMessageSender
|
||||
hexEncodedPublicKeys.forEach { hexEncodedPublicKey in
|
||||
var friendRequestStatus: LKFriendRequestStatus!
|
||||
storage.dbReadWriteConnection.readWrite { transaction in
|
||||
friendRequestStatus = self.storage.getFriendRequestStatus(for: hexEncodedPublicKey, transaction: transaction)
|
||||
}
|
||||
XCTAssert(friendRequestStatus == .requestSent)
|
||||
}
|
||||
*/
|
||||
// TODO: Test the case where Bob has multiple devices
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -1,47 +0,0 @@
|
|||
import XCTest
|
||||
|
||||
extension XCTestCase {
|
||||
|
||||
/// A helper for asynchronous testing.
|
||||
///
|
||||
/// Usage example:
|
||||
///
|
||||
/// ```
|
||||
/// func testSomething() {
|
||||
/// doAsyncThings()
|
||||
/// eventually {
|
||||
/// /* XCTAssert goes here... */
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The provided closure won't execute until `timeout` seconds have passed. Pass
|
||||
/// in a timeout long enough for your asynchronous process to finish if it's
|
||||
/// expected to take more than the default 0.1 second.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - timeout: number of seconds to wait before executing `closure`.
|
||||
/// - closure: a closure to execute when `timeout` seconds have passed.
|
||||
///
|
||||
/// - Note: `timeout` must be less than 60 seconds.
|
||||
func eventually(timeout: TimeInterval = 0.1, closure: @escaping () -> Void) {
|
||||
assert(timeout < 60)
|
||||
let expectation = self.expectation(description: "")
|
||||
expectation.fulfillAfter(timeout)
|
||||
self.waitForExpectations(timeout: 60) { _ in
|
||||
closure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension XCTestExpectation {
|
||||
|
||||
/// Call `fulfill()` after some time.
|
||||
///
|
||||
/// - Parameter time: number of seconds after which `fulfill()` will be called.
|
||||
func fulfillAfter(_ time: TimeInterval) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + time) {
|
||||
self.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>LokiPushNotificationService</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.usernotifications.service</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).NotificationServiceExtension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,254 +0,0 @@
|
|||
import UserNotifications
|
||||
import SessionServiceKit
|
||||
import SignalMessaging
|
||||
|
||||
final class NotificationServiceExtension : UNNotificationServiceExtension {
|
||||
static let isFromRemoteKey = "remote"
|
||||
static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId"
|
||||
|
||||
private var didPerformSetup = false
|
||||
|
||||
var areVersionMigrationsComplete = false
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var notificationContent: UNMutableNotificationContent?
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
self.contentHandler = contentHandler
|
||||
notificationContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
|
||||
var isMainAppActive = false
|
||||
if let sharedUserDefaults = UserDefaults(suiteName: "group.com.loki-project.loki-messenger") {
|
||||
isMainAppActive = sharedUserDefaults.bool(forKey: "isMainAppActive")
|
||||
}
|
||||
// If the main app is running, skip the whole process
|
||||
guard !isMainAppActive else { return self.completeWithFailure(content: notificationContent!) }
|
||||
|
||||
// The code using DispatchQueue.main.async { self.setUpIfNecessary() { Modify the notification content } } will somehow cause a freeze when a second PN comes
|
||||
|
||||
DispatchQueue.main.sync { self.setUpIfNecessary() {} }
|
||||
|
||||
AppReadiness.runNowOrWhenAppDidBecomeReady {
|
||||
if let notificationContent = self.notificationContent {
|
||||
// Modify the notification content here...
|
||||
let base64EncodedData = notificationContent.userInfo["ENCRYPTED_DATA"] as! String
|
||||
let data = Data(base64Encoded: base64EncodedData)!
|
||||
let decrypter = SSKEnvironment.shared.messageDecrypter
|
||||
let messageManager = SSKEnvironment.shared.messageManager
|
||||
if let envelope = try? MessageWrapper.unwrap(data: data), let data = try? envelope.serializedData() {
|
||||
let wasReceivedByUD = self.wasReceivedByUD(envelope: envelope)
|
||||
decrypter.decryptEnvelope(envelope,
|
||||
envelopeData: data,
|
||||
successBlock: { result, transaction in
|
||||
if let envelope = try? SSKProtoEnvelope.parseData(result.envelopeData) {
|
||||
messageManager.throws_processEnvelope(envelope, plaintextData: result.plaintextData, wasReceivedByUD: wasReceivedByUD, transaction: transaction, serverID: 0)
|
||||
self.handleDecryptionResult(result: result, notificationContent: notificationContent, transaction: transaction)
|
||||
} else {
|
||||
self.completeWithFailure(content: notificationContent)
|
||||
}
|
||||
},
|
||||
failureBlock: {
|
||||
self.completeWithFailure(content: notificationContent)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
self.completeWithFailure(content: notificationContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleDecryptionResult(result: OWSMessageDecryptResult, notificationContent: UNMutableNotificationContent, transaction: YapDatabaseReadWriteTransaction) {
|
||||
let contentProto = try? SSKProtoContent.parseData(result.plaintextData!)
|
||||
var thread: TSThread
|
||||
var newNotificationBody = ""
|
||||
let masterPublicKey = OWSPrimaryStorage.shared().getMasterHexEncodedPublicKey(for: result.source, in: transaction) ?? result.source
|
||||
var displayName = OWSUserProfile.fetch(uniqueId: masterPublicKey, transaction: transaction)?.profileName ?? SSKEnvironment.shared.contactsManager.displayName(forPhoneIdentifier: masterPublicKey)
|
||||
if let groupID = contentProto?.dataMessage?.group?.id {
|
||||
thread = TSGroupThread.getOrCreateThread(withGroupId: groupID, groupType: .closedGroup, transaction: transaction)
|
||||
var groupName = thread.name()
|
||||
if groupName.count < 1 {
|
||||
groupName = MessageStrings.newGroupDefaultTitle
|
||||
}
|
||||
let senderName = OWSUserProfile.fetch(uniqueId: masterPublicKey, transaction: transaction)?.profileName ?? SSKEnvironment.shared.contactsManager.displayName(forPhoneIdentifier: masterPublicKey)
|
||||
displayName = String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderName, groupName)
|
||||
let group: SSKProtoGroupContext = contentProto!.dataMessage!.group!
|
||||
let oldGroupModel = (thread as! TSGroupThread).groupModel
|
||||
let removedMembers = NSMutableSet(array: oldGroupModel.groupMemberIds)
|
||||
let newGroupModel = TSGroupModel.init(title: group.name,
|
||||
memberIds:group.members,
|
||||
image: oldGroupModel.groupImage,
|
||||
groupId: group.id,
|
||||
groupType: oldGroupModel.groupType,
|
||||
adminIds: group.admins)
|
||||
removedMembers.minus(Set(newGroupModel.groupMemberIds))
|
||||
newGroupModel.removedMembers = removedMembers
|
||||
switch contentProto?.dataMessage?.group?.type {
|
||||
case .update:
|
||||
newNotificationBody = oldGroupModel.getInfoStringAboutUpdate(to: newGroupModel, contactsManager: SSKEnvironment.shared.contactsManager)
|
||||
break
|
||||
case .quit:
|
||||
let nameString = SSKEnvironment.shared.contactsManager.displayName(forPhoneIdentifier: masterPublicKey, transaction: transaction)
|
||||
newNotificationBody = NSLocalizedString("GROUP_MEMBER_LEFT", comment: nameString)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
thread = TSContactThread.getOrCreateThread(withContactId: result.source, transaction: transaction)
|
||||
}
|
||||
let userInfo: [String:Any] = [ NotificationServiceExtension.threadIdKey : thread.uniqueId!, NotificationServiceExtension.isFromRemoteKey : true ]
|
||||
notificationContent.title = displayName
|
||||
notificationContent.userInfo = userInfo
|
||||
notificationContent.badge = 1
|
||||
if let attachment = contentProto?.dataMessage?.attachments.last {
|
||||
newNotificationBody = TSAttachment.emoji(forMimeType: attachment.contentType!) + "Attachment"
|
||||
if let rawMessageBody = contentProto?.dataMessage?.body, rawMessageBody.count > 0 {
|
||||
newNotificationBody += ": \(rawMessageBody)"
|
||||
}
|
||||
}
|
||||
if newNotificationBody.count < 1 {
|
||||
newNotificationBody = contentProto?.dataMessage?.body ?? "You've got a new message"
|
||||
}
|
||||
newNotificationBody = handleMentionIfNecessary(rawMessageBody: newNotificationBody, threadID: thread.uniqueId!, transaction: transaction)
|
||||
|
||||
let notificationPreference = Environment.shared.preferences
|
||||
if let notificationType = notificationPreference?.notificationPreviewType() {
|
||||
switch notificationType {
|
||||
case .nameNoPreview:
|
||||
notificationContent.body = "New Message"
|
||||
case .noNameNoPreview:
|
||||
notificationContent.title = ""
|
||||
notificationContent.body = "New Message"
|
||||
default:
|
||||
notificationContent.body = newNotificationBody
|
||||
}
|
||||
} else {
|
||||
notificationContent.body = newNotificationBody
|
||||
}
|
||||
|
||||
if notificationContent.body.count < 1 {
|
||||
self.completeWithFailure(content: notificationContent)
|
||||
} else {
|
||||
self.contentHandler!(notificationContent)
|
||||
}
|
||||
}
|
||||
|
||||
func handleMentionIfNecessary(rawMessageBody: String, threadID: String, transaction: YapDatabaseReadWriteTransaction) -> String {
|
||||
var string = rawMessageBody
|
||||
let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]*", options: [])
|
||||
var outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: string.utf16.count))
|
||||
while let match = outerMatch {
|
||||
let publicKey = String((string as NSString).substring(with: match.range).dropFirst()) // Drop the @
|
||||
let matchEnd: Int
|
||||
let displayName: String? = OWSProfileManager.shared().profileNameForRecipient(withID: publicKey, transaction: transaction)
|
||||
if let displayName = displayName {
|
||||
string = (string as NSString).replacingCharacters(in: match.range, with: "@\(displayName)")
|
||||
matchEnd = match.range.location + displayName.utf16.count
|
||||
} else {
|
||||
matchEnd = match.range.location + match.range.length
|
||||
}
|
||||
outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: matchEnd, length: string.utf16.count - matchEnd))
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
func setUpIfNecessary(completion: @escaping () -> Void) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// The NSE will often re-use the same process, so if we're
|
||||
// already set up we want to do nothing; we're already ready
|
||||
// to process new messages.
|
||||
guard !didPerformSetup else { return }
|
||||
|
||||
didPerformSetup = true
|
||||
|
||||
// This should be the first thing we do.
|
||||
SetCurrentAppContext(NotificationServiceExtensionContext())
|
||||
|
||||
DebugLogger.shared().enableTTYLogging()
|
||||
if _isDebugAssertConfiguration() {
|
||||
DebugLogger.shared().enableFileLogging()
|
||||
}
|
||||
|
||||
_ = AppVersion.sharedInstance()
|
||||
|
||||
Cryptography.seedRandom()
|
||||
|
||||
// We should never receive a non-voip notification on an app that doesn't support
|
||||
// app extensions since we have to inform the service we wanted these, so in theory
|
||||
// this path should never occur. However, the service does have our push token
|
||||
// so it is possible that could change in the future. If it does, do nothing
|
||||
// and don't disturb the user. Messages will be processed when they open the app.
|
||||
guard OWSPreferences.isReadyForAppExtensions() else { return completeSilenty() }
|
||||
|
||||
AppSetup.setupEnvironment(
|
||||
appSpecificSingletonBlock: {
|
||||
SSKEnvironment.shared.callMessageHandler = NoopCallMessageHandler()
|
||||
SSKEnvironment.shared.notificationsManager = NoopNotificationsManager()
|
||||
},
|
||||
migrationCompletion: { [weak self] in
|
||||
self?.versionMigrationsDidComplete()
|
||||
completion()
|
||||
}
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(storageIsReady),
|
||||
name: .StorageIsReady,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
// Called just before the extension will be terminated by the system.
|
||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||
if let contentHandler = contentHandler, let notificationContent = notificationContent {
|
||||
contentHandler(notificationContent)
|
||||
}
|
||||
}
|
||||
|
||||
func wasReceivedByUD(envelope: SSKProtoEnvelope) -> Bool {
|
||||
return (envelope.type == .unidentifiedSender && (!envelope.hasSource || envelope.source!.count < 1))
|
||||
}
|
||||
|
||||
@objc
|
||||
func versionMigrationsDidComplete() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
areVersionMigrationsComplete = true
|
||||
|
||||
checkIsAppReady()
|
||||
}
|
||||
|
||||
@objc
|
||||
func storageIsReady() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
checkIsAppReady()
|
||||
}
|
||||
|
||||
@objc
|
||||
func checkIsAppReady() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// Only mark the app as ready once.
|
||||
guard !AppReadiness.isAppReady() else { return }
|
||||
|
||||
// App isn't ready until storage is ready AND all version migrations are complete.
|
||||
guard OWSStorage.isStorageReady() && areVersionMigrationsComplete else { return }
|
||||
|
||||
// Note that this does much more than set a flag; it will also run all deferred blocks.
|
||||
AppReadiness.setAppIsReady()
|
||||
}
|
||||
|
||||
func completeSilenty() {
|
||||
contentHandler?(.init())
|
||||
}
|
||||
|
||||
func completeWithFailure(content: UNMutableNotificationContent) {
|
||||
content.body = "You've got a new message"
|
||||
content.title = "Session"
|
||||
let userInfo: [String:Any] = [NotificationServiceExtension.isFromRemoteKey : true]
|
||||
content.userInfo = userInfo
|
||||
contentHandler?(content)
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SessionServiceKit
|
||||
import SignalMessaging
|
||||
|
||||
final class NotificationServiceExtensionContext : NSObject, AppContext {
|
||||
let appLaunchTime = Date()
|
||||
let isMainApp = false
|
||||
let isMainAppAndActive = false
|
||||
|
||||
var openSystemSettingsAction: UIAlertAction?
|
||||
var wasWokenUpByPushNotification = true
|
||||
|
||||
var shouldProcessIncomingMessages: Bool { true }
|
||||
|
||||
lazy var buildTime: Date = {
|
||||
guard let buildTimestamp = Bundle.main.object(forInfoDictionaryKey: "BuildTimestamp") as? TimeInterval, buildTimestamp > 0 else {
|
||||
print("[Loki] No build timestamp; assuming app never expires.")
|
||||
return .distantFuture
|
||||
}
|
||||
return .init(timeIntervalSince1970: buildTimestamp)
|
||||
}()
|
||||
|
||||
override init() { super.init() }
|
||||
|
||||
func canPresentNotifications() -> Bool { true }
|
||||
func isAppForegroundAndActive() -> Bool { false }
|
||||
func isInBackground() -> Bool { true }
|
||||
func mainApplicationStateOnLaunch() -> UIApplication.State { .inactive }
|
||||
|
||||
func appDatabaseBaseDirectoryPath() -> String {
|
||||
return appSharedDataDirectoryPath()
|
||||
}
|
||||
|
||||
func appDocumentDirectoryPath() -> String {
|
||||
guard let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last else {
|
||||
preconditionFailure("Couldn't get document directory.")
|
||||
}
|
||||
return documentDirectoryURL.path
|
||||
}
|
||||
|
||||
func appSharedDataDirectoryPath() -> String {
|
||||
guard let groupContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: SignalApplicationGroup) else {
|
||||
preconditionFailure("Couldn't get shared data directory.")
|
||||
}
|
||||
return groupContainerURL.path
|
||||
}
|
||||
|
||||
func appUserDefaults() -> UserDefaults {
|
||||
guard let userDefaults = UserDefaults(suiteName: SignalApplicationGroup) else {
|
||||
preconditionFailure("Couldn't set up shared user defaults.")
|
||||
}
|
||||
return userDefaults
|
||||
}
|
||||
|
||||
func keychainStorage() -> SSKKeychainStorage {
|
||||
return SSKDefaultKeychainStorage.shared
|
||||
}
|
||||
|
||||
// MARK: - Currently Unused
|
||||
|
||||
let frame = CGRect.zero
|
||||
let interfaceOrientation = UIInterfaceOrientation.unknown
|
||||
let isRTL = false
|
||||
let isRunningTests = false
|
||||
let reportedApplicationState = UIApplication.State.background
|
||||
let statusBarHeight = CGFloat.zero
|
||||
|
||||
var mainWindow: UIWindow?
|
||||
|
||||
func beginBackgroundTask(expirationHandler: @escaping BackgroundTaskExpirationHandler) -> UIBackgroundTaskIdentifier { .invalid }
|
||||
func beginBackgroundTask(expirationHandler: @escaping BackgroundTaskExpirationHandler) -> UInt { 0 }
|
||||
func endBackgroundTask(_ backgroundTaskIdentifier: UIBackgroundTaskIdentifier) { }
|
||||
func endBackgroundTask(_ backgroundTaskIdentifier: UInt) { }
|
||||
func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjects: [Any]) { }
|
||||
func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjectsDescription: String) { }
|
||||
func frontmostViewController() -> UIViewController? { nil }
|
||||
func runNowOr(whenMainAppIsActive block: @escaping AppActiveBlock) { }
|
||||
func setMainAppBadgeNumber(_ value: Int) { }
|
||||
func setNetworkActivityIndicatorVisible(_ value: Bool) { }
|
||||
func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { }
|
||||
}
|
45
Makefile
45
Makefile
|
@ -1,45 +0,0 @@
|
|||
# Make sure we're failing even though we pipe to xcpretty
|
||||
SHELL=/bin/bash -o pipefail -o errexit
|
||||
|
||||
WORKING_DIR = ./
|
||||
THIRD_PARTY_DIR = $(WORKING_DIR)/ThirdParty
|
||||
SCHEME = Signal
|
||||
XCODE_BUILD = xcrun xcodebuild -workspace $(SCHEME).xcworkspace -scheme $(SCHEME) -sdk iphonesimulator
|
||||
|
||||
.PHONY: build test retest clean dependencies
|
||||
|
||||
default: test
|
||||
|
||||
update_dependencies:
|
||||
bundle exec pod update
|
||||
carthage update --platform iOS
|
||||
|
||||
setup:
|
||||
rbenv install -s
|
||||
gem install bundler
|
||||
bundle install
|
||||
|
||||
dependencies:
|
||||
cd $(WORKING_DIR) && \
|
||||
git submodule update --init
|
||||
cd $(THIRD_PARTY_DIR) && \
|
||||
carthage build --platform iOS
|
||||
|
||||
build: dependencies
|
||||
cd $(WORKING_DIR) && \
|
||||
$(XCODE_BUILD) build | xcpretty
|
||||
|
||||
test:
|
||||
bundle exec fastlane test
|
||||
|
||||
clean: clean_carthage
|
||||
cd $(WORKING_DIR) && \
|
||||
$(XCODE_BUILD) clean | xcpretty
|
||||
|
||||
clean_carthage:
|
||||
cd $(THIRD_PARTY_DIR) && \
|
||||
rm -fr Carthage/Build
|
||||
|
||||
# Migrating across swift versions requires me to run this sometimes
|
||||
clean_carthage_cache:
|
||||
rm -fr ~/Library/Caches/org.carthage.CarthageKit/
|
195
Podfile
195
Podfile
|
@ -4,113 +4,109 @@ source 'https://github.com/CocoaPods/Specs.git'
|
|||
use_frameworks!
|
||||
inhibit_all_warnings!
|
||||
|
||||
def shared_pods
|
||||
|
||||
###
|
||||
# OWS Pods
|
||||
###
|
||||
|
||||
pod 'SessionCoreKit', git: 'https://github.com/loki-project/session-ios-core-kit.git', testspecs: ["Tests"] # Fork of SignalCoreKit
|
||||
# pod 'SignalCoreKit', path: '../SignalCoreKit', testspecs: ["Tests"]
|
||||
|
||||
pod 'SessionAxolotlKit', git: 'https://github.com/loki-project/session-ios-protocol-kit.git', branch: 'master', testspecs: ["Tests"] # Fork of AxolotlKit
|
||||
# pod 'AxolotlKit', path: '../SignalProtocolKit', testspecs: ["Tests"]
|
||||
|
||||
pod 'SessionHKDFKit', git: 'https://github.com/nielsandriesse/session-ios-hkdf-kit.git', testspecs: ["Tests"] # Fork of HKDFKit
|
||||
# pod 'HKDFKit', path: '../HKDFKit', testspecs: ["Tests"]
|
||||
|
||||
pod 'SessionCurve25519Kit', git: 'https://github.com/loki-project/session-ios-curve-25519-kit', testspecs: ["Tests"] # Fork of Curve25519Kit
|
||||
# pod 'Curve25519Kit', path: '../Curve25519Kit', testspecs: ["Tests"]
|
||||
|
||||
pod 'SessionMetadataKit', git: 'https://github.com/loki-project/session-ios-metadata-kit', testspecs: ["Tests"] # Fork of SignalMetadataKit
|
||||
# pod 'SignalMetadataKit', path: '../SignalMetadataKit', testspecs: ["Tests"]
|
||||
|
||||
pod 'SessionServiceKit', path: '.', testspecs: ["Tests"]
|
||||
|
||||
# Project does not compile with PromiseKit 6.7.1
|
||||
# see: https://github.com/mxcl/PromiseKit/issues/990
|
||||
pod 'PromiseKit', "6.5.3"
|
||||
|
||||
###
|
||||
# forked third party pods
|
||||
###
|
||||
|
||||
# Includes some soon to be released "unencrypted header" changes required for the Share Extension
|
||||
pod 'SQLCipher', ">= 4.0.1"
|
||||
|
||||
# Forked for performance optimizations that are not likely to be upstreamed as they are specific
|
||||
# to our limited use of Mantle
|
||||
pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master'
|
||||
# pod 'Mantle', path: '../Mantle'
|
||||
|
||||
# Forked for compatibily with the ShareExtension, changes have an open PR, but have not been merged.
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/signalapp/YapDatabase.git', branch: 'signal-release'
|
||||
# pod 'YapDatabase/SQLCipher', path: '../YapDatabase'
|
||||
|
||||
# Forked to incorporate our self-built binary artifact.
|
||||
pod 'GRKOpenSSLFramework', git: 'https://github.com/signalapp/GRKOpenSSLFramework'
|
||||
#pod 'GRKOpenSSLFramework', path: '../GRKOpenSSLFramework'
|
||||
|
||||
pod 'Starscream', git: 'https://github.com/signalapp/Starscream.git', branch: 'signal-release'
|
||||
# pod 'Starscream', path: '../Starscream'
|
||||
|
||||
###
|
||||
# third party pods
|
||||
###
|
||||
|
||||
pod 'AFNetworking', '~> 3.2.1', inhibit_warnings: true
|
||||
target 'Session' do
|
||||
pod 'AFNetworking', inhibit_warnings: true
|
||||
pod 'CryptoSwift', :inhibit_warnings => true
|
||||
pod 'FeedKit', :inhibit_warnings => true
|
||||
pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master', :inhibit_warnings => true
|
||||
pod 'NVActivityIndicatorView', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'PureLayout', '~> 3.1.4', :inhibit_warnings => true
|
||||
pod 'Reachability', :inhibit_warnings => true
|
||||
pod 'YYImage', git: 'https://github.com/signalapp/YYImage', :inhibit_warnings => true
|
||||
pod 'ZXingObjC', '~> 3.6.4', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'Signal' do
|
||||
project 'Signal'
|
||||
shared_pods
|
||||
pod 'Sodium', :inhibit_warnings => true
|
||||
pod 'SSZipArchive', :inhibit_warnings => true
|
||||
|
||||
###
|
||||
# Loki third party pods
|
||||
###
|
||||
|
||||
pod 'CryptoSwift', '~> 1.3', :inhibit_warnings => true
|
||||
pod 'FeedKit', '~> 8.1', :inhibit_warnings => true
|
||||
pod 'NVActivityIndicatorView', '~> 4.7', :inhibit_warnings => true
|
||||
pod 'Sodium', '~> 0.8.0', :inhibit_warnings => true
|
||||
|
||||
target 'SignalTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
pod 'Starscream', git: 'https://github.com/signalapp/Starscream.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/signalapp/YapDatabase.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YYImage', git: 'https://github.com/signalapp/YYImage', :inhibit_warnings => true
|
||||
pod 'ZXingObjC', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'SignalShareExtension' do
|
||||
project 'Signal'
|
||||
shared_pods
|
||||
target 'SessionShareExtension' do
|
||||
pod 'AFNetworking', inhibit_warnings: true
|
||||
pod 'CryptoSwift', :inhibit_warnings => true
|
||||
pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true
|
||||
pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'PureLayout', '~> 3.1.4', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/signalapp/YapDatabase.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'LokiPushNotificationService' do
|
||||
project 'Signal'
|
||||
shared_pods
|
||||
|
||||
###
|
||||
# Loki third party pods
|
||||
###
|
||||
|
||||
pod 'CryptoSwift', '~> 1.3', :inhibit_warnings => true
|
||||
target 'SessionPushNotificationExtension' do
|
||||
pod 'AFNetworking', inhibit_warnings: true
|
||||
pod 'CryptoSwift', :inhibit_warnings => true
|
||||
pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true
|
||||
pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'PureLayout', '~> 3.1.4', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/signalapp/YapDatabase.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'SignalMessaging' do
|
||||
project 'Signal'
|
||||
shared_pods
|
||||
target 'SignalUtilitiesKit' do
|
||||
pod 'AFNetworking', inhibit_warnings: true
|
||||
pod 'CryptoSwift', :inhibit_warnings => true
|
||||
pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true
|
||||
pod 'GRKOpenSSLFramework', :inhibit_warnings => true
|
||||
pod 'HKDFKit', :inhibit_warnings => true
|
||||
pod 'libPhoneNumber-iOS', :inhibit_warnings => true
|
||||
pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'PureLayout', '~> 3.1.4', :inhibit_warnings => true
|
||||
pod 'Reachability', :inhibit_warnings => true
|
||||
pod 'SAMKeychain', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
pod 'Starscream', git: 'https://github.com/signalapp/Starscream.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'SwiftProtobuf', '~> 1.5.0', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/signalapp/YapDatabase.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YYImage', git: 'https://github.com/signalapp/YYImage', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'SessionUIKit' do
|
||||
|
||||
end
|
||||
|
||||
target 'SessionMessagingKit' do
|
||||
pod 'AFNetworking', inhibit_warnings: true
|
||||
pod 'CryptoSwift', :inhibit_warnings => true
|
||||
pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
pod 'SwiftProtobuf', '~> 1.5.0', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'SessionProtocolKit' do
|
||||
pod 'CocoaLumberjack', :inhibit_warnings => true
|
||||
pod 'CryptoSwift', :inhibit_warnings => true
|
||||
pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true
|
||||
pod 'GRKOpenSSLFramework', :inhibit_warnings => true
|
||||
pod 'HKDFKit', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
pod 'SwiftProtobuf', '~> 1.5.0', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'SessionSnodeKit' do
|
||||
pod 'CryptoSwift', :inhibit_warnings => true
|
||||
pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'SessionUtilitiesKit' do
|
||||
pod 'CryptoSwift', :inhibit_warnings => true
|
||||
pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
enable_whole_module_optimization_for_cryptoswift(installer)
|
||||
enable_extension_support_for_purelayout(installer)
|
||||
enable_whole_module_optimization_for_crypto_swift(installer)
|
||||
enable_extension_support_for_pure_layout(installer)
|
||||
set_minimum_deployment_target(installer)
|
||||
end
|
||||
|
||||
def enable_whole_module_optimization_for_cryptoswift(installer)
|
||||
def enable_whole_module_optimization_for_crypto_swift(installer)
|
||||
installer.pods_project.targets.each do |target|
|
||||
if target.name.end_with? "CryptoSwift"
|
||||
target.build_configurations.each do |config|
|
||||
|
@ -121,15 +117,22 @@ def enable_whole_module_optimization_for_cryptoswift(installer)
|
|||
end
|
||||
end
|
||||
|
||||
# PureLayout by default makes use of UIApplication, and must be configured to be built for an extension.
|
||||
def enable_extension_support_for_purelayout(installer)
|
||||
def enable_extension_support_for_pure_layout(installer)
|
||||
installer.pods_project.targets.each do |target|
|
||||
if target.name.end_with? "PureLayout"
|
||||
target.build_configurations.each do |build_configuration|
|
||||
if build_configuration.build_settings['APPLICATION_EXTENSION_API_ONLY'] == 'YES'
|
||||
build_configuration.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = ['$(inherited)', 'PURELAYOUT_APP_EXTENSIONS=1']
|
||||
build_configuration.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = [ '$(inherited)', 'PURELAYOUT_APP_EXTENSIONS=1' ]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_minimum_deployment_target(installer)
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |build_configuration|
|
||||
build_configuration.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
231
Podfile.lock
231
Podfile.lock
|
@ -1,124 +1,51 @@
|
|||
PODS:
|
||||
- AFNetworking (3.2.1):
|
||||
- AFNetworking/NSURLSession (= 3.2.1)
|
||||
- AFNetworking/Reachability (= 3.2.1)
|
||||
- AFNetworking/Security (= 3.2.1)
|
||||
- AFNetworking/Serialization (= 3.2.1)
|
||||
- AFNetworking/UIKit (= 3.2.1)
|
||||
- AFNetworking/NSURLSession (3.2.1):
|
||||
- AFNetworking (4.0.1):
|
||||
- AFNetworking/NSURLSession (= 4.0.1)
|
||||
- AFNetworking/Reachability (= 4.0.1)
|
||||
- AFNetworking/Security (= 4.0.1)
|
||||
- AFNetworking/Serialization (= 4.0.1)
|
||||
- AFNetworking/UIKit (= 4.0.1)
|
||||
- AFNetworking/NSURLSession (4.0.1):
|
||||
- AFNetworking/Reachability
|
||||
- AFNetworking/Security
|
||||
- AFNetworking/Serialization
|
||||
- AFNetworking/Reachability (3.2.1)
|
||||
- AFNetworking/Security (3.2.1)
|
||||
- AFNetworking/Serialization (3.2.1)
|
||||
- AFNetworking/UIKit (3.2.1):
|
||||
- AFNetworking/Reachability (4.0.1)
|
||||
- AFNetworking/Security (4.0.1)
|
||||
- AFNetworking/Serialization (4.0.1)
|
||||
- AFNetworking/UIKit (4.0.1):
|
||||
- AFNetworking/NSURLSession
|
||||
- CocoaLumberjack (3.6.2):
|
||||
- CocoaLumberjack/Core (= 3.6.2)
|
||||
- CocoaLumberjack/Core (3.6.2)
|
||||
- CryptoSwift (1.3.2)
|
||||
- FeedKit (8.1.1)
|
||||
- GRKOpenSSLFramework (1.0.2.12)
|
||||
- Curve25519Kit (2.1.0):
|
||||
- CocoaLumberjack
|
||||
- SignalCoreKit
|
||||
- FeedKit (9.1.2)
|
||||
- GRKOpenSSLFramework (1.0.2.20)
|
||||
- HKDFKit (0.0.3)
|
||||
- libPhoneNumber-iOS (0.9.15)
|
||||
- Mantle (2.1.0):
|
||||
- Mantle/extobjc (= 2.1.0)
|
||||
- Mantle/extobjc (2.1.0)
|
||||
- NVActivityIndicatorView (4.8.0):
|
||||
- NVActivityIndicatorView/Presenter (= 4.8.0)
|
||||
- NVActivityIndicatorView/Presenter (4.8.0)
|
||||
- PromiseKit (6.5.3):
|
||||
- PromiseKit/CorePromise (= 6.5.3)
|
||||
- PromiseKit/Foundation (= 6.5.3)
|
||||
- PromiseKit/UIKit (= 6.5.3)
|
||||
- PromiseKit/CorePromise (6.5.3)
|
||||
- PromiseKit/Foundation (6.5.3):
|
||||
- NVActivityIndicatorView (5.0.1):
|
||||
- NVActivityIndicatorView/Base (= 5.0.1)
|
||||
- NVActivityIndicatorView/Base (5.0.1)
|
||||
- PromiseKit (6.13.1):
|
||||
- PromiseKit/CorePromise (= 6.13.1)
|
||||
- PromiseKit/Foundation (= 6.13.1)
|
||||
- PromiseKit/UIKit (= 6.13.1)
|
||||
- PromiseKit/CorePromise (6.13.1)
|
||||
- PromiseKit/Foundation (6.13.1):
|
||||
- PromiseKit/CorePromise
|
||||
- PromiseKit/UIKit (6.5.3):
|
||||
- PromiseKit/UIKit (6.13.1):
|
||||
- PromiseKit/CorePromise
|
||||
- PureLayout (3.1.6)
|
||||
- Reachability (3.2)
|
||||
- SAMKeychain (1.5.3)
|
||||
- SessionAxolotlKit (1.0.7):
|
||||
- CocoaLumberjack
|
||||
- SessionCoreKit (~> 1.0.0)
|
||||
- SessionCurve25519Kit (~> 2.1.2)
|
||||
- SessionHKDFKit (~> 0.0.5)
|
||||
- SwiftProtobuf (~> 1.5.0)
|
||||
- SessionAxolotlKit/Tests (1.0.7):
|
||||
- CocoaLumberjack
|
||||
- SessionCoreKit (~> 1.0.0)
|
||||
- SessionCurve25519Kit (~> 2.1.2)
|
||||
- SessionHKDFKit (~> 0.0.5)
|
||||
- SwiftProtobuf (~> 1.5.0)
|
||||
- SessionCoreKit (1.0.0):
|
||||
- SignalCoreKit (1.0.0):
|
||||
- CocoaLumberjack
|
||||
- GRKOpenSSLFramework
|
||||
- SessionCoreKit/Tests (1.0.0):
|
||||
- CocoaLumberjack
|
||||
- GRKOpenSSLFramework
|
||||
- SessionCurve25519Kit (2.1.3):
|
||||
- CocoaLumberjack
|
||||
- SessionCoreKit (~> 1.0.0)
|
||||
- SessionCurve25519Kit/Tests (2.1.3):
|
||||
- CocoaLumberjack
|
||||
- SessionCoreKit (~> 1.0.0)
|
||||
- SessionHKDFKit (0.0.5):
|
||||
- CocoaLumberjack
|
||||
- SessionCoreKit
|
||||
- SessionHKDFKit/Tests (0.0.5):
|
||||
- CocoaLumberjack
|
||||
- SessionCoreKit
|
||||
- SessionMetadataKit (1.0.9):
|
||||
- CocoaLumberjack
|
||||
- CryptoSwift (~> 1.3)
|
||||
- SessionAxolotlKit (~> 1.0.7)
|
||||
- SessionCoreKit (~> 1.0.0)
|
||||
- SessionCurve25519Kit (~> 2.1.2)
|
||||
- SessionHKDFKit (~> 0.0.5)
|
||||
- SwiftProtobuf (~> 1.5.0)
|
||||
- SessionMetadataKit/Tests (1.0.9):
|
||||
- CocoaLumberjack
|
||||
- CryptoSwift (~> 1.3)
|
||||
- SessionAxolotlKit (~> 1.0.7)
|
||||
- SessionCoreKit (~> 1.0.0)
|
||||
- SessionCurve25519Kit (~> 2.1.2)
|
||||
- SessionHKDFKit (~> 0.0.5)
|
||||
- SwiftProtobuf (~> 1.5.0)
|
||||
- SessionServiceKit (1.0.0):
|
||||
- AFNetworking
|
||||
- CocoaLumberjack
|
||||
- CryptoSwift (~> 1.3)
|
||||
- GRKOpenSSLFramework
|
||||
- libPhoneNumber-iOS
|
||||
- Mantle
|
||||
- PromiseKit (~> 6.0)
|
||||
- Reachability
|
||||
- SAMKeychain
|
||||
- SessionAxolotlKit (~> 1.0.7)
|
||||
- SessionCoreKit (~> 1.0.0)
|
||||
- SessionCurve25519Kit (~> 2.1.3)
|
||||
- SessionMetadataKit (~> 1.0.9)
|
||||
- Starscream
|
||||
- SwiftProtobuf (~> 1.5.0)
|
||||
- YapDatabase/SQLCipher
|
||||
- SessionServiceKit/Tests (1.0.0):
|
||||
- AFNetworking
|
||||
- CocoaLumberjack
|
||||
- CryptoSwift (~> 1.3)
|
||||
- GRKOpenSSLFramework
|
||||
- libPhoneNumber-iOS
|
||||
- Mantle
|
||||
- PromiseKit (~> 6.0)
|
||||
- Reachability
|
||||
- SAMKeychain
|
||||
- SessionAxolotlKit (~> 1.0.7)
|
||||
- SessionCoreKit (~> 1.0.0)
|
||||
- SessionCurve25519Kit (~> 2.1.3)
|
||||
- SessionMetadataKit (~> 1.0.9)
|
||||
- Starscream
|
||||
- SwiftProtobuf (~> 1.5.0)
|
||||
- YapDatabase/SQLCipher
|
||||
- Sodium (0.8.0)
|
||||
- SQLCipher (4.4.0):
|
||||
- SQLCipher/standard (= 4.4.0)
|
||||
|
@ -198,34 +125,28 @@ PODS:
|
|||
- ZXingObjC/All (3.6.5)
|
||||
|
||||
DEPENDENCIES:
|
||||
- AFNetworking (~> 3.2.1)
|
||||
- CryptoSwift (~> 1.3)
|
||||
- FeedKit (~> 8.1)
|
||||
- GRKOpenSSLFramework (from `https://github.com/signalapp/GRKOpenSSLFramework`)
|
||||
- AFNetworking
|
||||
- CocoaLumberjack
|
||||
- CryptoSwift
|
||||
- Curve25519Kit (from `https://github.com/signalapp/Curve25519Kit.git`)
|
||||
- FeedKit
|
||||
- GRKOpenSSLFramework
|
||||
- HKDFKit
|
||||
- libPhoneNumber-iOS
|
||||
- Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`)
|
||||
- NVActivityIndicatorView (~> 4.7)
|
||||
- PromiseKit (= 6.5.3)
|
||||
- NVActivityIndicatorView
|
||||
- PromiseKit
|
||||
- PureLayout (~> 3.1.4)
|
||||
- Reachability
|
||||
- SessionAxolotlKit (from `https://github.com/loki-project/session-ios-protocol-kit.git`, branch `master`)
|
||||
- SessionAxolotlKit/Tests (from `https://github.com/loki-project/session-ios-protocol-kit.git`, branch `master`)
|
||||
- SessionCoreKit (from `https://github.com/loki-project/session-ios-core-kit.git`)
|
||||
- SessionCoreKit/Tests (from `https://github.com/loki-project/session-ios-core-kit.git`)
|
||||
- SessionCurve25519Kit (from `https://github.com/loki-project/session-ios-curve-25519-kit`)
|
||||
- SessionCurve25519Kit/Tests (from `https://github.com/loki-project/session-ios-curve-25519-kit`)
|
||||
- SessionHKDFKit (from `https://github.com/nielsandriesse/session-ios-hkdf-kit.git`)
|
||||
- SessionHKDFKit/Tests (from `https://github.com/nielsandriesse/session-ios-hkdf-kit.git`)
|
||||
- SessionMetadataKit (from `https://github.com/loki-project/session-ios-metadata-kit`)
|
||||
- SessionMetadataKit/Tests (from `https://github.com/loki-project/session-ios-metadata-kit`)
|
||||
- SessionServiceKit (from `.`)
|
||||
- SessionServiceKit/Tests (from `.`)
|
||||
- Sodium (~> 0.8.0)
|
||||
- SQLCipher (>= 4.0.1)
|
||||
- SAMKeychain
|
||||
- SignalCoreKit (from `https://github.com/signalapp/SignalCoreKit.git`)
|
||||
- Sodium
|
||||
- SSZipArchive
|
||||
- Starscream (from `https://github.com/signalapp/Starscream.git`, branch `signal-release`)
|
||||
- SwiftProtobuf (~> 1.5.0)
|
||||
- YapDatabase/SQLCipher (from `https://github.com/signalapp/YapDatabase.git`, branch `signal-release`)
|
||||
- YYImage (from `https://github.com/signalapp/YYImage`)
|
||||
- ZXingObjC (~> 3.6.4)
|
||||
- ZXingObjC
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/CocoaPods/Specs.git:
|
||||
|
@ -233,6 +154,8 @@ SPEC REPOS:
|
|||
- CocoaLumberjack
|
||||
- CryptoSwift
|
||||
- FeedKit
|
||||
- GRKOpenSSLFramework
|
||||
- HKDFKit
|
||||
- libPhoneNumber-iOS
|
||||
- NVActivityIndicatorView
|
||||
- PromiseKit
|
||||
|
@ -246,24 +169,13 @@ SPEC REPOS:
|
|||
- ZXingObjC
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
GRKOpenSSLFramework:
|
||||
:git: https://github.com/signalapp/GRKOpenSSLFramework
|
||||
Curve25519Kit:
|
||||
:git: https://github.com/signalapp/Curve25519Kit.git
|
||||
Mantle:
|
||||
:branch: signal-master
|
||||
:git: https://github.com/signalapp/Mantle
|
||||
SessionAxolotlKit:
|
||||
:branch: master
|
||||
:git: https://github.com/loki-project/session-ios-protocol-kit.git
|
||||
SessionCoreKit:
|
||||
:git: https://github.com/loki-project/session-ios-core-kit.git
|
||||
SessionCurve25519Kit:
|
||||
:git: https://github.com/loki-project/session-ios-curve-25519-kit
|
||||
SessionHKDFKit:
|
||||
:git: https://github.com/nielsandriesse/session-ios-hkdf-kit.git
|
||||
SessionMetadataKit:
|
||||
:git: https://github.com/loki-project/session-ios-metadata-kit
|
||||
SessionServiceKit:
|
||||
:path: "."
|
||||
SignalCoreKit:
|
||||
:git: https://github.com/signalapp/SignalCoreKit.git
|
||||
Starscream:
|
||||
:branch: signal-release
|
||||
:git: https://github.com/signalapp/Starscream.git
|
||||
|
@ -274,27 +186,15 @@ EXTERNAL SOURCES:
|
|||
:git: https://github.com/signalapp/YYImage
|
||||
|
||||
CHECKOUT OPTIONS:
|
||||
GRKOpenSSLFramework:
|
||||
:commit: b799c27e7927e5304ec1e4ad53c6d33c6fd1cae7
|
||||
:git: https://github.com/signalapp/GRKOpenSSLFramework
|
||||
Curve25519Kit:
|
||||
:commit: 4fc1c10e98fff2534b5379a9bb587430fdb8e577
|
||||
:git: https://github.com/signalapp/Curve25519Kit.git
|
||||
Mantle:
|
||||
:commit: b72c2d1e6132501db906de2cffa8ded7803c54f4
|
||||
:git: https://github.com/signalapp/Mantle
|
||||
SessionAxolotlKit:
|
||||
:commit: be92fccb6152ee02c8c2658cb3c2e21201f119d1
|
||||
:git: https://github.com/loki-project/session-ios-protocol-kit.git
|
||||
SessionCoreKit:
|
||||
:commit: 0d66c90657b62cb66ecd2767c57408a951650f23
|
||||
:git: https://github.com/loki-project/session-ios-core-kit.git
|
||||
SessionCurve25519Kit:
|
||||
:commit: c3bc075d1e1c8339eebe2af184869de1a007d855
|
||||
:git: https://github.com/loki-project/session-ios-curve-25519-kit
|
||||
SessionHKDFKit:
|
||||
:commit: 0dcf8cf8a7995ef8663146f7063e6c1d7f5a3274
|
||||
:git: https://github.com/nielsandriesse/session-ios-hkdf-kit.git
|
||||
SessionMetadataKit:
|
||||
:commit: df787d84bb8adb23c10df669296dee8d7988e410
|
||||
:git: https://github.com/loki-project/session-ios-metadata-kit
|
||||
SignalCoreKit:
|
||||
:commit: 21c092e94b307690957b50f2305e5e65d28fa89e
|
||||
:git: https://github.com/signalapp/SignalCoreKit.git
|
||||
Starscream:
|
||||
:commit: b09ea163c3cb305152c65b299cb024610f52e735
|
||||
:git: https://github.com/signalapp/Starscream.git
|
||||
|
@ -306,24 +206,21 @@ CHECKOUT OPTIONS:
|
|||
:git: https://github.com/signalapp/YYImage
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
AFNetworking: b6f891fdfaed196b46c7a83cf209e09697b94057
|
||||
AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce
|
||||
CocoaLumberjack: bd155f2dd06c0e0b03f876f7a3ee55693122ec94
|
||||
CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060
|
||||
FeedKit: 3418eed25f0b493b205b4de1b8511ac21d413fa9
|
||||
GRKOpenSSLFramework: 8a3735ad41e7dc1daff460467bccd32ca5d6ae3e
|
||||
Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6
|
||||
FeedKit: 71653273ab08e618cd6fd1301ca08fc02dca6a9e
|
||||
GRKOpenSSLFramework: dc635b0a9d4cd8af2a9ff80a61e779e21b69dfd8
|
||||
HKDFKit: c058305d6f64b84f28c50bd7aa89574625bcb62a
|
||||
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
|
||||
Mantle: 2fa750afa478cd625a94230fbf1c13462f29395b
|
||||
NVActivityIndicatorView: d24b7ebcf80af5dcd994adb650e2b6c93379270f
|
||||
PromiseKit: c609029bdd801f792551a504c695c7d3098b42cd
|
||||
NVActivityIndicatorView: 738e843cb8924e9e4fc3e559d0728031624bf860
|
||||
PromiseKit: 28fda91c973cc377875d8c0ea4f973013c05b6db
|
||||
PureLayout: bd3c4ec3a3819ad387c99ebb72c6b129c3ed4d2d
|
||||
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SessionAxolotlKit: f3558573a3cb52ebb921572f2f3b683da5eddad9
|
||||
SessionCoreKit: 778a3f6e3da788b43497734166646025b6392e88
|
||||
SessionCurve25519Kit: 9bb9afe199e4bc23578a4b15932ad2c57bd047b1
|
||||
SessionHKDFKit: b0f4e669411703ab925aba07491c5611564d1419
|
||||
SessionMetadataKit: d37afdc47d20c7046faa139a92e68fa99f76c95b
|
||||
SessionServiceKit: b12afb3975b33a9579802111f948838861d914bb
|
||||
SignalCoreKit: 4562b2bbd9830077439ca003f952a798457d4ea5
|
||||
Sodium: 63c0ca312a932e6da481689537d4b35568841bdc
|
||||
SQLCipher: e434ed542b24f38ea7b36468a13f9765e1b5c072
|
||||
SSZipArchive: 62d4947b08730e4cda640473b0066d209ff033c9
|
||||
|
@ -333,6 +230,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: 8b58cc2d282fa528aa81b128b643596a9059c270
|
||||
PODFILE CHECKSUM: 1aad5fbd49d168d2f50da0b3b3e515247f1ce291
|
||||
|
||||
COCOAPODS: 1.10.0.rc.1
|
||||
|
|
1
Pods
1
Pods
|
@ -1 +0,0 @@
|
|||
Subproject commit d1b2c2c2fe1b47ab1314192e9320f6cbc30871be
|
|
@ -1,106 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import datetime
|
||||
import argparse
|
||||
import commands
|
||||
import re
|
||||
|
||||
git_repo_path = os.path.abspath(subprocess.check_output(['git', 'rev-parse', '--show-toplevel']).strip())
|
||||
|
||||
string_h_functions = [
|
||||
"memchr",
|
||||
"memcmp",
|
||||
"memcpy",
|
||||
"memmove",
|
||||
"memset",
|
||||
"strcat",
|
||||
"strchr",
|
||||
"strcmp",
|
||||
"strcoll",
|
||||
"strcpy",
|
||||
"strcspn",
|
||||
"strerror",
|
||||
"strlen",
|
||||
"strncat",
|
||||
"strncmp",
|
||||
"strncpy",
|
||||
"strpbrk",
|
||||
"strrchr",
|
||||
"strspn",
|
||||
"strstr",
|
||||
"strtok",
|
||||
"strxfrm",
|
||||
"strtok_r",
|
||||
"strerror_r",
|
||||
"strdup",
|
||||
"memccpy",
|
||||
"stpcpy",
|
||||
"stpncpy",
|
||||
"strndup",
|
||||
"strnlen",
|
||||
"strsignal",
|
||||
"memset_s",
|
||||
"memmem",
|
||||
"memset_pattern4",
|
||||
"memset_pattern8",
|
||||
"memset_pattern16",
|
||||
"strcasestr",
|
||||
"strnstr",
|
||||
"strlcat",
|
||||
"strlcpy",
|
||||
"strmode",
|
||||
"strsep",
|
||||
"swab",
|
||||
"timingsafe_bcmp",
|
||||
]
|
||||
|
||||
|
||||
def process_if_appropriate(file_path):
|
||||
file_ext = os.path.splitext(file_path)[1]
|
||||
if file_ext.lower() not in ('.c', '.cpp', '.m', '.mm', '.h', '.swift'):
|
||||
return
|
||||
# print 'file_path', file_path, 'file_ext', file_ext
|
||||
|
||||
with open(file_path, 'rt') as f:
|
||||
text = f.read()
|
||||
|
||||
has_match = False
|
||||
for string_h_function in string_h_functions:
|
||||
regex = re.compile(string_h_function + r'\s*\(')
|
||||
assert(regex)
|
||||
matches = []
|
||||
for match in regex.finditer(text):
|
||||
matches.append(match)
|
||||
# matches = regex.findall(text)
|
||||
if not matches:
|
||||
continue
|
||||
if not has_match:
|
||||
has_match = True
|
||||
print 'file_path', file_path, 'file_ext', file_ext
|
||||
for match in matches:
|
||||
# print 'match', match, type(match)
|
||||
print '\t', 'match:', match.group(0)
|
||||
if has_match:
|
||||
print
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(description='Precommit script.')
|
||||
parser.add_argument('--path', help='used to specify a path to process.')
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
if args.path:
|
||||
dir_path = args.path
|
||||
else:
|
||||
dir_path = git_repo_path
|
||||
|
||||
for rootdir, dirnames, filenames in os.walk(dir_path):
|
||||
for filename in filenames:
|
||||
file_path = os.path.abspath(os.path.join(rootdir, filename))
|
||||
process_if_appropriate(file_path)
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
git reset --hard HEAD
|
||||
git clean -xdff
|
|
@ -1,206 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import commands
|
||||
import subprocess
|
||||
import argparse
|
||||
import inspect
|
||||
|
||||
def fail(message):
|
||||
file_name = __file__
|
||||
current_line_no = inspect.stack()[1][2]
|
||||
current_function_name = inspect.stack()[1][3]
|
||||
print 'Failure in:', file_name, current_line_no, current_function_name
|
||||
print message
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def execute_command(command):
|
||||
try:
|
||||
print ' '.join(command)
|
||||
output = subprocess.check_output(command)
|
||||
if output:
|
||||
print output
|
||||
except subprocess.CalledProcessError as e:
|
||||
print e.output
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def find_project_root():
|
||||
path = os.path.abspath(os.curdir)
|
||||
|
||||
while True:
|
||||
# print 'path', path
|
||||
if not os.path.exists(path):
|
||||
break
|
||||
git_path = os.path.join(path, '.git')
|
||||
if os.path.exists(git_path):
|
||||
return path
|
||||
new_path = os.path.abspath(os.path.dirname(path))
|
||||
if not new_path or new_path == path:
|
||||
break
|
||||
path = new_path
|
||||
|
||||
fail('Could not find project root path')
|
||||
|
||||
|
||||
def is_valid_release_version(value):
|
||||
regex = re.compile(r'^(\d+)\.(\d+)\.(\d+)$')
|
||||
match = regex.search(value)
|
||||
return match is not None
|
||||
|
||||
|
||||
def is_valid_build_version(value):
|
||||
regex = re.compile(r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$')
|
||||
match = regex.search(value)
|
||||
return match is not None
|
||||
|
||||
|
||||
def set_versions(plist_file_path, release_version, build_version):
|
||||
if not is_valid_release_version(release_version):
|
||||
fail('Invalid release version: %s' % release_version)
|
||||
if not is_valid_build_version(build_version):
|
||||
fail('Invalid build version: %s' % build_version)
|
||||
|
||||
with open(plist_file_path, 'rt') as f:
|
||||
text = f.read()
|
||||
# print 'text', text
|
||||
|
||||
# The "short" version is the release number.
|
||||
#
|
||||
# <key>CFBundleShortVersionString</key>
|
||||
# <string>2.20.0</string>
|
||||
file_regex = re.compile(r'<key>CFBundleShortVersionString</key>\s*<string>([\d\.]+)</string>', re.MULTILINE)
|
||||
file_match = file_regex.search(text)
|
||||
# print 'match', match
|
||||
if not file_match:
|
||||
fail('Could not parse .plist')
|
||||
text = text[:file_match.start(1)] + release_version + text[file_match.end(1):]
|
||||
|
||||
# The "long" version is the build number.
|
||||
#
|
||||
# <key>CFBundleVersion</key>
|
||||
# <string>2.20.0.3</string>
|
||||
file_regex = re.compile(r'<key>CFBundleVersion</key>\s*<string>([\d\.]+)</string>', re.MULTILINE)
|
||||
file_match = file_regex.search(text)
|
||||
# print 'match', match
|
||||
if not file_match:
|
||||
fail('Could not parse .plist')
|
||||
text = text[:file_match.start(1)] + build_version + text[file_match.end(1):]
|
||||
|
||||
with open(plist_file_path, 'wt') as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
def get_versions(plist_file_path):
|
||||
with open(plist_file_path, 'rt') as f:
|
||||
text = f.read()
|
||||
# print 'text', text
|
||||
|
||||
# <key>CFBundleVersion</key>
|
||||
# <string>2.13.0.13</string>
|
||||
file_regex = re.compile(r'<key>CFBundleVersion</key>\s*<string>([\d\.]+)</string>', re.MULTILINE)
|
||||
file_match = file_regex.search(text)
|
||||
# print 'match', match
|
||||
if not file_match:
|
||||
fail('Could not parse .plist')
|
||||
|
||||
# e.g. "2.13.0.13"
|
||||
old_build_version = file_match.group(1)
|
||||
print 'old_build_version:', old_build_version
|
||||
|
||||
if not is_valid_build_version(old_build_version):
|
||||
fail('Invalid build version: %s' % old_build_version)
|
||||
|
||||
build_number_regex = re.compile(r'\.(\d+)$')
|
||||
build_number_match = build_number_regex.search(old_build_version)
|
||||
if not build_number_match:
|
||||
fail('Could not parse .plist version')
|
||||
|
||||
# e.g. "13"
|
||||
old_build_number = build_number_match.group(1)
|
||||
print 'old_build_number:', old_build_number
|
||||
|
||||
release_number_regex = re.compile(r'^(.+)\.\d+$')
|
||||
release_number_match = release_number_regex.search(old_build_version)
|
||||
if not release_number_match:
|
||||
fail('Could not parse .plist')
|
||||
|
||||
# e.g. "2.13.0"
|
||||
old_release_version = release_number_match.group(1)
|
||||
print 'old_release_version:', old_release_version
|
||||
|
||||
# Given "2.13.0.13", this should return "2.13.0" and "13" as strings.
|
||||
return old_release_version, old_build_number
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Precommit cleanup script.')
|
||||
parser.add_argument('--version', help='used for starting a new version.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
project_root_path = find_project_root()
|
||||
# print 'project_root_path', project_root_path
|
||||
# plist_path
|
||||
main_plist_path = os.path.join(project_root_path, 'Signal', 'Signal-Info.plist')
|
||||
if not os.path.exists(main_plist_path):
|
||||
fail('Could not find main app info .plist')
|
||||
share_ext_plist_path = os.path.join(project_root_path, 'SignalShareExtension', 'Info.plist')
|
||||
if not os.path.exists(share_ext_plist_path):
|
||||
fail('Could not find share extension info .plist')
|
||||
|
||||
output = subprocess.check_output(['git', 'status', '--porcelain'])
|
||||
if len(output.strip()) > 0:
|
||||
print output
|
||||
fail('Git repository has untracked files.')
|
||||
output = subprocess.check_output(['git', 'diff', '--shortstat'])
|
||||
if len(output.strip()) > 0:
|
||||
print output
|
||||
fail('Git repository has untracked files.')
|
||||
|
||||
# Ensure .plist is in xml format, not binary.
|
||||
output = subprocess.check_output(['plutil', '-convert', 'xml1', main_plist_path])
|
||||
output = subprocess.check_output(['plutil', '-convert', 'xml1', share_ext_plist_path])
|
||||
# print 'output', output
|
||||
|
||||
# ---------------
|
||||
# Main App
|
||||
# ---------------
|
||||
|
||||
old_release_version, old_build_number = get_versions(main_plist_path)
|
||||
|
||||
if args.version:
|
||||
# e.g. --version 1.2.3 -> "1.2.3", "1.2.3.0"
|
||||
new_release_version = args.version.strip()
|
||||
new_build_version = new_release_version + ".0"
|
||||
else:
|
||||
new_build_number = str(1 + int(old_build_number))
|
||||
print 'new_build_number:', new_build_number
|
||||
|
||||
new_release_version = old_release_version
|
||||
new_build_version = old_release_version + "." + new_build_number
|
||||
|
||||
print 'new_release_version:', new_release_version
|
||||
print 'new_build_version:', new_build_version
|
||||
|
||||
set_versions(main_plist_path, new_release_version, new_build_version)
|
||||
|
||||
# ---------------
|
||||
# Share Extension
|
||||
# ---------------
|
||||
|
||||
set_versions(share_ext_plist_path, new_release_version, new_build_version)
|
||||
|
||||
# ---------------
|
||||
# Git
|
||||
# ---------------
|
||||
command = ['git', 'add', '.']
|
||||
execute_command(command)
|
||||
command = ['git', 'commit', '-m', '"Bump build to %s."' % new_build_version]
|
||||
execute_command(command)
|
||||
command = ['git', 'tag', new_build_version]
|
||||
execute_command(command)
|
||||
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import commands
|
||||
import subprocess
|
||||
import argparse
|
||||
import inspect
|
||||
import urllib2
|
||||
import json
|
||||
|
||||
def fail(message):
|
||||
file_name = __file__
|
||||
current_line_no = inspect.stack()[1][2]
|
||||
current_function_name = inspect.stack()[1][3]
|
||||
print 'Failure in:', file_name, current_line_no, current_function_name
|
||||
print message
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def execute_command(command):
|
||||
try:
|
||||
print ' '.join(command)
|
||||
output = subprocess.check_output(command)
|
||||
if output:
|
||||
print output
|
||||
except subprocess.CalledProcessError as e:
|
||||
print e.output
|
||||
sys.exit(1)
|
||||
|
||||
def add_field(curl_command, form_key, form_value):
|
||||
curl_command.append('-F')
|
||||
curl_command.append("%s=%s" % (form_key, form_value))
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Precommit cleanup script.')
|
||||
parser.add_argument('--file', required=True, help='used for starting a new version.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
params_response = urllib2.urlopen("https://debuglogs.org/").read()
|
||||
|
||||
params = json.loads(params_response)
|
||||
|
||||
upload_url = params['url']
|
||||
upload_fields = params['fields']
|
||||
|
||||
upload_key = upload_fields.pop('key')
|
||||
upload_key = upload_key + os.path.splitext(args.file)[1]
|
||||
|
||||
download_url = 'https://debuglogs.org/' + upload_key
|
||||
print 'download_url:', download_url
|
||||
|
||||
curl_command = ['curl', '-v', '-i', '-X', 'POST']
|
||||
|
||||
# key must appear before other fields
|
||||
add_field(curl_command, 'key', upload_key)
|
||||
for field_name in upload_fields:
|
||||
add_field(curl_command, field_name, upload_fields[field_name])
|
||||
|
||||
add_field(curl_command, "content-type", "application/octet-stream")
|
||||
|
||||
curl_command.append('-F')
|
||||
curl_command.append("file=@%s" % (args.file,))
|
||||
curl_command.append(upload_url)
|
||||
|
||||
print ' '.join(curl_command)
|
||||
|
||||
print 'Running...'
|
||||
execute_command(curl_command)
|
||||
|
||||
print 'download_url:', download_url
|
||||
|
|
@ -1,679 +0,0 @@
|
|||
# emoji-data.txt
|
||||
# Date: 2017-08-10, 11:51:32 GMT
|
||||
# © 2017 Unicode®, Inc.
|
||||
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
|
||||
# For terms of use, see http://www.unicode.org/terms_of_use.html
|
||||
#
|
||||
# Emoji Data for UTR #51
|
||||
# Version: 6.0
|
||||
#
|
||||
# For documentation and usage, see http://www.unicode.org/reports/tr51
|
||||
#
|
||||
# Format:
|
||||
# <codepoint(s)> ; <property> # <comments>
|
||||
# Note: there is no guarantee as to the structure of whitespace or comments
|
||||
#
|
||||
# Characters and sequences are listed in code point order. Users should be shown a more natural order.
|
||||
# See the CLDR collation order for Emoji.
|
||||
|
||||
|
||||
# ================================================
|
||||
|
||||
# All omitted code points have Emoji=No
|
||||
# @missing: 0000..10FFFF ; Emoji ; No
|
||||
|
||||
0023 ; Emoji # 1.1 [1] (#️) number sign
|
||||
002A ; Emoji # 1.1 [1] (*️) asterisk
|
||||
0030..0039 ; Emoji # 1.1 [10] (0️..9️) digit zero..digit nine
|
||||
00A9 ; Emoji # 1.1 [1] (©️) copyright
|
||||
00AE ; Emoji # 1.1 [1] (®️) registered
|
||||
203C ; Emoji # 1.1 [1] (‼️) double exclamation mark
|
||||
2049 ; Emoji # 3.0 [1] (⁉️) exclamation question mark
|
||||
2122 ; Emoji # 1.1 [1] (™️) trade mark
|
||||
2139 ; Emoji # 3.0 [1] (ℹ️) information
|
||||
2194..2199 ; Emoji # 1.1 [6] (↔️..↙️) left-right arrow..down-left arrow
|
||||
21A9..21AA ; Emoji # 1.1 [2] (↩️..↪️) right arrow curving left..left arrow curving right
|
||||
231A..231B ; Emoji # 1.1 [2] (⌚..⌛) watch..hourglass done
|
||||
2328 ; Emoji # 1.1 [1] (⌨️) keyboard
|
||||
23CF ; Emoji # 4.0 [1] (⏏️) eject button
|
||||
23E9..23F3 ; Emoji # 6.0 [11] (⏩..⏳) fast-forward button..hourglass not done
|
||||
23F8..23FA ; Emoji # 7.0 [3] (⏸️..⏺️) pause button..record button
|
||||
24C2 ; Emoji # 1.1 [1] (Ⓜ️) circled M
|
||||
25AA..25AB ; Emoji # 1.1 [2] (▪️..▫️) black small square..white small square
|
||||
25B6 ; Emoji # 1.1 [1] (▶️) play button
|
||||
25C0 ; Emoji # 1.1 [1] (◀️) reverse button
|
||||
25FB..25FE ; Emoji # 3.2 [4] (◻️..◾) white medium square..black medium-small square
|
||||
2600..2604 ; Emoji # 1.1 [5] (☀️..☄️) sun..comet
|
||||
260E ; Emoji # 1.1 [1] (☎️) telephone
|
||||
2611 ; Emoji # 1.1 [1] (☑️) ballot box with check
|
||||
2614..2615 ; Emoji # 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
|
||||
2618 ; Emoji # 4.1 [1] (☘️) shamrock
|
||||
261D ; Emoji # 1.1 [1] (☝️) index pointing up
|
||||
2620 ; Emoji # 1.1 [1] (☠️) skull and crossbones
|
||||
2622..2623 ; Emoji # 1.1 [2] (☢️..☣️) radioactive..biohazard
|
||||
2626 ; Emoji # 1.1 [1] (☦️) orthodox cross
|
||||
262A ; Emoji # 1.1 [1] (☪️) star and crescent
|
||||
262E..262F ; Emoji # 1.1 [2] (☮️..☯️) peace symbol..yin yang
|
||||
2638..263A ; Emoji # 1.1 [3] (☸️..☺️) wheel of dharma..smiling face
|
||||
2640 ; Emoji # 1.1 [1] (♀️) female sign
|
||||
2642 ; Emoji # 1.1 [1] (♂️) male sign
|
||||
2648..2653 ; Emoji # 1.1 [12] (♈..♓) Aries..Pisces
|
||||
2660 ; Emoji # 1.1 [1] (♠️) spade suit
|
||||
2663 ; Emoji # 1.1 [1] (♣️) club suit
|
||||
2665..2666 ; Emoji # 1.1 [2] (♥️..♦️) heart suit..diamond suit
|
||||
2668 ; Emoji # 1.1 [1] (♨️) hot springs
|
||||
267B ; Emoji # 3.2 [1] (♻️) recycling symbol
|
||||
267F ; Emoji # 4.1 [1] (♿) wheelchair symbol
|
||||
2692..2697 ; Emoji # 4.1 [6] (⚒️..⚗️) hammer and pick..alembic
|
||||
2699 ; Emoji # 4.1 [1] (⚙️) gear
|
||||
269B..269C ; Emoji # 4.1 [2] (⚛️..⚜️) atom symbol..fleur-de-lis
|
||||
26A0..26A1 ; Emoji # 4.0 [2] (⚠️..⚡) warning..high voltage
|
||||
26AA..26AB ; Emoji # 4.1 [2] (⚪..⚫) white circle..black circle
|
||||
26B0..26B1 ; Emoji # 4.1 [2] (⚰️..⚱️) coffin..funeral urn
|
||||
26BD..26BE ; Emoji # 5.2 [2] (⚽..⚾) soccer ball..baseball
|
||||
26C4..26C5 ; Emoji # 5.2 [2] (⛄..⛅) snowman without snow..sun behind cloud
|
||||
26C8 ; Emoji # 5.2 [1] (⛈️) cloud with lightning and rain
|
||||
26CE ; Emoji # 6.0 [1] (⛎) Ophiuchus
|
||||
26CF ; Emoji # 5.2 [1] (⛏️) pick
|
||||
26D1 ; Emoji # 5.2 [1] (⛑️) rescue worker’s helmet
|
||||
26D3..26D4 ; Emoji # 5.2 [2] (⛓️..⛔) chains..no entry
|
||||
26E9..26EA ; Emoji # 5.2 [2] (⛩️..⛪) shinto shrine..church
|
||||
26F0..26F5 ; Emoji # 5.2 [6] (⛰️..⛵) mountain..sailboat
|
||||
26F7..26FA ; Emoji # 5.2 [4] (⛷️..⛺) skier..tent
|
||||
26FD ; Emoji # 5.2 [1] (⛽) fuel pump
|
||||
2702 ; Emoji # 1.1 [1] (✂️) scissors
|
||||
2705 ; Emoji # 6.0 [1] (✅) white heavy check mark
|
||||
2708..2709 ; Emoji # 1.1 [2] (✈️..✉️) airplane..envelope
|
||||
270A..270B ; Emoji # 6.0 [2] (✊..✋) raised fist..raised hand
|
||||
270C..270D ; Emoji # 1.1 [2] (✌️..✍️) victory hand..writing hand
|
||||
270F ; Emoji # 1.1 [1] (✏️) pencil
|
||||
2712 ; Emoji # 1.1 [1] (✒️) black nib
|
||||
2714 ; Emoji # 1.1 [1] (✔️) heavy check mark
|
||||
2716 ; Emoji # 1.1 [1] (✖️) heavy multiplication x
|
||||
271D ; Emoji # 1.1 [1] (✝️) latin cross
|
||||
2721 ; Emoji # 1.1 [1] (✡️) star of David
|
||||
2728 ; Emoji # 6.0 [1] (✨) sparkles
|
||||
2733..2734 ; Emoji # 1.1 [2] (✳️..✴️) eight-spoked asterisk..eight-pointed star
|
||||
2744 ; Emoji # 1.1 [1] (❄️) snowflake
|
||||
2747 ; Emoji # 1.1 [1] (❇️) sparkle
|
||||
274C ; Emoji # 6.0 [1] (❌) cross mark
|
||||
274E ; Emoji # 6.0 [1] (❎) cross mark button
|
||||
2753..2755 ; Emoji # 6.0 [3] (❓..❕) question mark..white exclamation mark
|
||||
2757 ; Emoji # 5.2 [1] (❗) exclamation mark
|
||||
2763..2764 ; Emoji # 1.1 [2] (❣️..❤️) heavy heart exclamation..red heart
|
||||
2795..2797 ; Emoji # 6.0 [3] (➕..➗) heavy plus sign..heavy division sign
|
||||
27A1 ; Emoji # 1.1 [1] (➡️) right arrow
|
||||
27B0 ; Emoji # 6.0 [1] (➰) curly loop
|
||||
27BF ; Emoji # 6.0 [1] (➿) double curly loop
|
||||
2934..2935 ; Emoji # 3.2 [2] (⤴️..⤵️) right arrow curving up..right arrow curving down
|
||||
2B05..2B07 ; Emoji # 4.0 [3] (⬅️..⬇️) left arrow..down arrow
|
||||
2B1B..2B1C ; Emoji # 5.1 [2] (⬛..⬜) black large square..white large square
|
||||
2B50 ; Emoji # 5.1 [1] (⭐) white medium star
|
||||
2B55 ; Emoji # 5.2 [1] (⭕) heavy large circle
|
||||
3030 ; Emoji # 1.1 [1] (〰️) wavy dash
|
||||
303D ; Emoji # 3.2 [1] (〽️) part alternation mark
|
||||
3297 ; Emoji # 1.1 [1] (㊗️) Japanese “congratulations” button
|
||||
3299 ; Emoji # 1.1 [1] (㊙️) Japanese “secret” button
|
||||
1F004 ; Emoji # 5.1 [1] (🀄) mahjong red dragon
|
||||
1F0CF ; Emoji # 6.0 [1] (🃏) joker
|
||||
1F170..1F171 ; Emoji # 6.0 [2] (🅰️..🅱️) A button (blood type)..B button (blood type)
|
||||
1F17E ; Emoji # 6.0 [1] (🅾️) O button (blood type)
|
||||
1F17F ; Emoji # 5.2 [1] (🅿️) P button
|
||||
1F18E ; Emoji # 6.0 [1] (🆎) AB button (blood type)
|
||||
1F191..1F19A ; Emoji # 6.0 [10] (🆑..🆚) CL button..VS button
|
||||
1F1E6..1F1FF ; Emoji # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
|
||||
1F201..1F202 ; Emoji # 6.0 [2] (🈁..🈂️) Japanese “here” button..Japanese “service charge” button
|
||||
1F21A ; Emoji # 5.2 [1] (🈚) Japanese “free of charge” button
|
||||
1F22F ; Emoji # 5.2 [1] (🈯) Japanese “reserved” button
|
||||
1F232..1F23A ; Emoji # 6.0 [9] (🈲..🈺) Japanese “prohibited” button..Japanese “open for business” button
|
||||
1F250..1F251 ; Emoji # 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
|
||||
1F300..1F320 ; Emoji # 6.0 [33] (🌀..🌠) cyclone..shooting star
|
||||
1F321 ; Emoji # 7.0 [1] (🌡️) thermometer
|
||||
1F324..1F32C ; Emoji # 7.0 [9] (🌤️..🌬️) sun behind small cloud..wind face
|
||||
1F32D..1F32F ; Emoji # 8.0 [3] (🌭..🌯) hot dog..burrito
|
||||
1F330..1F335 ; Emoji # 6.0 [6] (🌰..🌵) chestnut..cactus
|
||||
1F336 ; Emoji # 7.0 [1] (🌶️) hot pepper
|
||||
1F337..1F37C ; Emoji # 6.0 [70] (🌷..🍼) tulip..baby bottle
|
||||
1F37D ; Emoji # 7.0 [1] (🍽️) fork and knife with plate
|
||||
1F37E..1F37F ; Emoji # 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
|
||||
1F380..1F393 ; Emoji # 6.0 [20] (🎀..🎓) ribbon..graduation cap
|
||||
1F396..1F397 ; Emoji # 7.0 [2] (🎖️..🎗️) military medal..reminder ribbon
|
||||
1F399..1F39B ; Emoji # 7.0 [3] (🎙️..🎛️) studio microphone..control knobs
|
||||
1F39E..1F39F ; Emoji # 7.0 [2] (🎞️..🎟️) film frames..admission tickets
|
||||
1F3A0..1F3C4 ; Emoji # 6.0 [37] (🎠..🏄) carousel horse..person surfing
|
||||
1F3C5 ; Emoji # 7.0 [1] (🏅) sports medal
|
||||
1F3C6..1F3CA ; Emoji # 6.0 [5] (🏆..🏊) trophy..person swimming
|
||||
1F3CB..1F3CE ; Emoji # 7.0 [4] (🏋️..🏎️) person lifting weights..racing car
|
||||
1F3CF..1F3D3 ; Emoji # 8.0 [5] (🏏..🏓) cricket game..ping pong
|
||||
1F3D4..1F3DF ; Emoji # 7.0 [12] (🏔️..🏟️) snow-capped mountain..stadium
|
||||
1F3E0..1F3F0 ; Emoji # 6.0 [17] (🏠..🏰) house..castle
|
||||
1F3F3..1F3F5 ; Emoji # 7.0 [3] (🏳️..🏵️) white flag..rosette
|
||||
1F3F7 ; Emoji # 7.0 [1] (🏷️) label
|
||||
1F3F8..1F3FF ; Emoji # 8.0 [8] (🏸..🏿) badminton..dark skin tone
|
||||
1F400..1F43E ; Emoji # 6.0 [63] (🐀..🐾) rat..paw prints
|
||||
1F43F ; Emoji # 7.0 [1] (🐿️) chipmunk
|
||||
1F440 ; Emoji # 6.0 [1] (👀) eyes
|
||||
1F441 ; Emoji # 7.0 [1] (👁️) eye
|
||||
1F442..1F4F7 ; Emoji # 6.0[182] (👂..📷) ear..camera
|
||||
1F4F8 ; Emoji # 7.0 [1] (📸) camera with flash
|
||||
1F4F9..1F4FC ; Emoji # 6.0 [4] (📹..📼) video camera..videocassette
|
||||
1F4FD ; Emoji # 7.0 [1] (📽️) film projector
|
||||
1F4FF ; Emoji # 8.0 [1] (📿) prayer beads
|
||||
1F500..1F53D ; Emoji # 6.0 [62] (🔀..🔽) shuffle tracks button..down button
|
||||
1F549..1F54A ; Emoji # 7.0 [2] (🕉️..🕊️) om..dove
|
||||
1F54B..1F54E ; Emoji # 8.0 [4] (🕋..🕎) kaaba..menorah
|
||||
1F550..1F567 ; Emoji # 6.0 [24] (🕐..🕧) one o’clock..twelve-thirty
|
||||
1F56F..1F570 ; Emoji # 7.0 [2] (🕯️..🕰️) candle..mantelpiece clock
|
||||
1F573..1F579 ; Emoji # 7.0 [7] (🕳️..🕹️) hole..joystick
|
||||
1F57A ; Emoji # 9.0 [1] (🕺) man dancing
|
||||
1F587 ; Emoji # 7.0 [1] (🖇️) linked paperclips
|
||||
1F58A..1F58D ; Emoji # 7.0 [4] (🖊️..🖍️) pen..crayon
|
||||
1F590 ; Emoji # 7.0 [1] (🖐️) hand with fingers splayed
|
||||
1F595..1F596 ; Emoji # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
|
||||
1F5A4 ; Emoji # 9.0 [1] (🖤) black heart
|
||||
1F5A5 ; Emoji # 7.0 [1] (🖥️) desktop computer
|
||||
1F5A8 ; Emoji # 7.0 [1] (🖨️) printer
|
||||
1F5B1..1F5B2 ; Emoji # 7.0 [2] (🖱️..🖲️) computer mouse..trackball
|
||||
1F5BC ; Emoji # 7.0 [1] (🖼️) framed picture
|
||||
1F5C2..1F5C4 ; Emoji # 7.0 [3] (🗂️..🗄️) card index dividers..file cabinet
|
||||
1F5D1..1F5D3 ; Emoji # 7.0 [3] (🗑️..🗓️) wastebasket..spiral calendar
|
||||
1F5DC..1F5DE ; Emoji # 7.0 [3] (🗜️..🗞️) clamp..rolled-up newspaper
|
||||
1F5E1 ; Emoji # 7.0 [1] (🗡️) dagger
|
||||
1F5E3 ; Emoji # 7.0 [1] (🗣️) speaking head
|
||||
1F5E8 ; Emoji # 7.0 [1] (🗨️) left speech bubble
|
||||
1F5EF ; Emoji # 7.0 [1] (🗯️) right anger bubble
|
||||
1F5F3 ; Emoji # 7.0 [1] (🗳️) ballot box with ballot
|
||||
1F5FA ; Emoji # 7.0 [1] (🗺️) world map
|
||||
1F5FB..1F5FF ; Emoji # 6.0 [5] (🗻..🗿) mount fuji..moai
|
||||
1F600 ; Emoji # 6.1 [1] (😀) grinning face
|
||||
1F601..1F610 ; Emoji # 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
|
||||
1F611 ; Emoji # 6.1 [1] (😑) expressionless face
|
||||
1F612..1F614 ; Emoji # 6.0 [3] (😒..😔) unamused face..pensive face
|
||||
1F615 ; Emoji # 6.1 [1] (😕) confused face
|
||||
1F616 ; Emoji # 6.0 [1] (😖) confounded face
|
||||
1F617 ; Emoji # 6.1 [1] (😗) kissing face
|
||||
1F618 ; Emoji # 6.0 [1] (😘) face blowing a kiss
|
||||
1F619 ; Emoji # 6.1 [1] (😙) kissing face with smiling eyes
|
||||
1F61A ; Emoji # 6.0 [1] (😚) kissing face with closed eyes
|
||||
1F61B ; Emoji # 6.1 [1] (😛) face with tongue
|
||||
1F61C..1F61E ; Emoji # 6.0 [3] (😜..😞) winking face with tongue..disappointed face
|
||||
1F61F ; Emoji # 6.1 [1] (😟) worried face
|
||||
1F620..1F625 ; Emoji # 6.0 [6] (😠..😥) angry face..sad but relieved face
|
||||
1F626..1F627 ; Emoji # 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
|
||||
1F628..1F62B ; Emoji # 6.0 [4] (😨..😫) fearful face..tired face
|
||||
1F62C ; Emoji # 6.1 [1] (😬) grimacing face
|
||||
1F62D ; Emoji # 6.0 [1] (😭) loudly crying face
|
||||
1F62E..1F62F ; Emoji # 6.1 [2] (😮..😯) face with open mouth..hushed face
|
||||
1F630..1F633 ; Emoji # 6.0 [4] (😰..😳) anxious face with sweat..flushed face
|
||||
1F634 ; Emoji # 6.1 [1] (😴) sleeping face
|
||||
1F635..1F640 ; Emoji # 6.0 [12] (😵..🙀) dizzy face..weary cat face
|
||||
1F641..1F642 ; Emoji # 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
|
||||
1F643..1F644 ; Emoji # 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
|
||||
1F645..1F64F ; Emoji # 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
|
||||
1F680..1F6C5 ; Emoji # 6.0 [70] (🚀..🛅) rocket..left luggage
|
||||
1F6CB..1F6CF ; Emoji # 7.0 [5] (🛋️..🛏️) couch and lamp..bed
|
||||
1F6D0 ; Emoji # 8.0 [1] (🛐) place of worship
|
||||
1F6D1..1F6D2 ; Emoji # 9.0 [2] (🛑..🛒) stop sign..shopping cart
|
||||
1F6E0..1F6E5 ; Emoji # 7.0 [6] (🛠️..🛥️) hammer and wrench..motor boat
|
||||
1F6E9 ; Emoji # 7.0 [1] (🛩️) small airplane
|
||||
1F6EB..1F6EC ; Emoji # 7.0 [2] (🛫..🛬) airplane departure..airplane arrival
|
||||
1F6F0 ; Emoji # 7.0 [1] (🛰️) satellite
|
||||
1F6F3 ; Emoji # 7.0 [1] (🛳️) passenger ship
|
||||
1F6F4..1F6F6 ; Emoji # 9.0 [3] (🛴..🛶) kick scooter..canoe
|
||||
1F6F7..1F6F8 ; Emoji # 10.0 [2] (🛷..🛸) sled..flying saucer
|
||||
1F910..1F918 ; Emoji # 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
|
||||
1F919..1F91E ; Emoji # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
|
||||
1F91F ; Emoji # 10.0 [1] (🤟) love-you gesture
|
||||
1F920..1F927 ; Emoji # 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
|
||||
1F928..1F92F ; Emoji # 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
|
||||
1F930 ; Emoji # 9.0 [1] (🤰) pregnant woman
|
||||
1F931..1F932 ; Emoji # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
|
||||
1F933..1F93A ; Emoji # 9.0 [8] (🤳..🤺) selfie..person fencing
|
||||
1F93C..1F93E ; Emoji # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
|
||||
1F940..1F945 ; Emoji # 9.0 [6] (🥀..🥅) wilted flower..goal net
|
||||
1F947..1F94B ; Emoji # 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
|
||||
1F94C ; Emoji # 10.0 [1] (🥌) curling stone
|
||||
1F950..1F95E ; Emoji # 9.0 [15] (🥐..🥞) croissant..pancakes
|
||||
1F95F..1F96B ; Emoji # 10.0 [13] (🥟..🥫) dumpling..canned food
|
||||
1F980..1F984 ; Emoji # 8.0 [5] (🦀..🦄) crab..unicorn face
|
||||
1F985..1F991 ; Emoji # 9.0 [13] (🦅..🦑) eagle..squid
|
||||
1F992..1F997 ; Emoji # 10.0 [6] (🦒..🦗) giraffe..cricket
|
||||
1F9C0 ; Emoji # 8.0 [1] (🧀) cheese wedge
|
||||
1F9D0..1F9E6 ; Emoji # 10.0 [23] (🧐..🧦) face with monocle..socks
|
||||
|
||||
# Total elements: 1182
|
||||
|
||||
# ================================================
|
||||
|
||||
# All omitted code points have Emoji_Presentation=No
|
||||
# @missing: 0000..10FFFF ; Emoji_Presentation ; No
|
||||
|
||||
231A..231B ; Emoji_Presentation # 1.1 [2] (⌚..⌛) watch..hourglass done
|
||||
23E9..23EC ; Emoji_Presentation # 6.0 [4] (⏩..⏬) fast-forward button..fast down button
|
||||
23F0 ; Emoji_Presentation # 6.0 [1] (⏰) alarm clock
|
||||
23F3 ; Emoji_Presentation # 6.0 [1] (⏳) hourglass not done
|
||||
25FD..25FE ; Emoji_Presentation # 3.2 [2] (◽..◾) white medium-small square..black medium-small square
|
||||
2614..2615 ; Emoji_Presentation # 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
|
||||
2648..2653 ; Emoji_Presentation # 1.1 [12] (♈..♓) Aries..Pisces
|
||||
267F ; Emoji_Presentation # 4.1 [1] (♿) wheelchair symbol
|
||||
2693 ; Emoji_Presentation # 4.1 [1] (⚓) anchor
|
||||
26A1 ; Emoji_Presentation # 4.0 [1] (⚡) high voltage
|
||||
26AA..26AB ; Emoji_Presentation # 4.1 [2] (⚪..⚫) white circle..black circle
|
||||
26BD..26BE ; Emoji_Presentation # 5.2 [2] (⚽..⚾) soccer ball..baseball
|
||||
26C4..26C5 ; Emoji_Presentation # 5.2 [2] (⛄..⛅) snowman without snow..sun behind cloud
|
||||
26CE ; Emoji_Presentation # 6.0 [1] (⛎) Ophiuchus
|
||||
26D4 ; Emoji_Presentation # 5.2 [1] (⛔) no entry
|
||||
26EA ; Emoji_Presentation # 5.2 [1] (⛪) church
|
||||
26F2..26F3 ; Emoji_Presentation # 5.2 [2] (⛲..⛳) fountain..flag in hole
|
||||
26F5 ; Emoji_Presentation # 5.2 [1] (⛵) sailboat
|
||||
26FA ; Emoji_Presentation # 5.2 [1] (⛺) tent
|
||||
26FD ; Emoji_Presentation # 5.2 [1] (⛽) fuel pump
|
||||
2705 ; Emoji_Presentation # 6.0 [1] (✅) white heavy check mark
|
||||
270A..270B ; Emoji_Presentation # 6.0 [2] (✊..✋) raised fist..raised hand
|
||||
2728 ; Emoji_Presentation # 6.0 [1] (✨) sparkles
|
||||
274C ; Emoji_Presentation # 6.0 [1] (❌) cross mark
|
||||
274E ; Emoji_Presentation # 6.0 [1] (❎) cross mark button
|
||||
2753..2755 ; Emoji_Presentation # 6.0 [3] (❓..❕) question mark..white exclamation mark
|
||||
2757 ; Emoji_Presentation # 5.2 [1] (❗) exclamation mark
|
||||
2795..2797 ; Emoji_Presentation # 6.0 [3] (➕..➗) heavy plus sign..heavy division sign
|
||||
27B0 ; Emoji_Presentation # 6.0 [1] (➰) curly loop
|
||||
27BF ; Emoji_Presentation # 6.0 [1] (➿) double curly loop
|
||||
2B1B..2B1C ; Emoji_Presentation # 5.1 [2] (⬛..⬜) black large square..white large square
|
||||
2B50 ; Emoji_Presentation # 5.1 [1] (⭐) white medium star
|
||||
2B55 ; Emoji_Presentation # 5.2 [1] (⭕) heavy large circle
|
||||
1F004 ; Emoji_Presentation # 5.1 [1] (🀄) mahjong red dragon
|
||||
1F0CF ; Emoji_Presentation # 6.0 [1] (🃏) joker
|
||||
1F18E ; Emoji_Presentation # 6.0 [1] (🆎) AB button (blood type)
|
||||
1F191..1F19A ; Emoji_Presentation # 6.0 [10] (🆑..🆚) CL button..VS button
|
||||
1F1E6..1F1FF ; Emoji_Presentation # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
|
||||
1F201 ; Emoji_Presentation # 6.0 [1] (🈁) Japanese “here” button
|
||||
1F21A ; Emoji_Presentation # 5.2 [1] (🈚) Japanese “free of charge” button
|
||||
1F22F ; Emoji_Presentation # 5.2 [1] (🈯) Japanese “reserved” button
|
||||
1F232..1F236 ; Emoji_Presentation # 6.0 [5] (🈲..🈶) Japanese “prohibited” button..Japanese “not free of charge” button
|
||||
1F238..1F23A ; Emoji_Presentation # 6.0 [3] (🈸..🈺) Japanese “application” button..Japanese “open for business” button
|
||||
1F250..1F251 ; Emoji_Presentation # 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
|
||||
1F300..1F320 ; Emoji_Presentation # 6.0 [33] (🌀..🌠) cyclone..shooting star
|
||||
1F32D..1F32F ; Emoji_Presentation # 8.0 [3] (🌭..🌯) hot dog..burrito
|
||||
1F330..1F335 ; Emoji_Presentation # 6.0 [6] (🌰..🌵) chestnut..cactus
|
||||
1F337..1F37C ; Emoji_Presentation # 6.0 [70] (🌷..🍼) tulip..baby bottle
|
||||
1F37E..1F37F ; Emoji_Presentation # 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
|
||||
1F380..1F393 ; Emoji_Presentation # 6.0 [20] (🎀..🎓) ribbon..graduation cap
|
||||
1F3A0..1F3C4 ; Emoji_Presentation # 6.0 [37] (🎠..🏄) carousel horse..person surfing
|
||||
1F3C5 ; Emoji_Presentation # 7.0 [1] (🏅) sports medal
|
||||
1F3C6..1F3CA ; Emoji_Presentation # 6.0 [5] (🏆..🏊) trophy..person swimming
|
||||
1F3CF..1F3D3 ; Emoji_Presentation # 8.0 [5] (🏏..🏓) cricket game..ping pong
|
||||
1F3E0..1F3F0 ; Emoji_Presentation # 6.0 [17] (🏠..🏰) house..castle
|
||||
1F3F4 ; Emoji_Presentation # 7.0 [1] (🏴) black flag
|
||||
1F3F8..1F3FF ; Emoji_Presentation # 8.0 [8] (🏸..🏿) badminton..dark skin tone
|
||||
1F400..1F43E ; Emoji_Presentation # 6.0 [63] (🐀..🐾) rat..paw prints
|
||||
1F440 ; Emoji_Presentation # 6.0 [1] (👀) eyes
|
||||
1F442..1F4F7 ; Emoji_Presentation # 6.0[182] (👂..📷) ear..camera
|
||||
1F4F8 ; Emoji_Presentation # 7.0 [1] (📸) camera with flash
|
||||
1F4F9..1F4FC ; Emoji_Presentation # 6.0 [4] (📹..📼) video camera..videocassette
|
||||
1F4FF ; Emoji_Presentation # 8.0 [1] (📿) prayer beads
|
||||
1F500..1F53D ; Emoji_Presentation # 6.0 [62] (🔀..🔽) shuffle tracks button..down button
|
||||
1F54B..1F54E ; Emoji_Presentation # 8.0 [4] (🕋..🕎) kaaba..menorah
|
||||
1F550..1F567 ; Emoji_Presentation # 6.0 [24] (🕐..🕧) one o’clock..twelve-thirty
|
||||
1F57A ; Emoji_Presentation # 9.0 [1] (🕺) man dancing
|
||||
1F595..1F596 ; Emoji_Presentation # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
|
||||
1F5A4 ; Emoji_Presentation # 9.0 [1] (🖤) black heart
|
||||
1F5FB..1F5FF ; Emoji_Presentation # 6.0 [5] (🗻..🗿) mount fuji..moai
|
||||
1F600 ; Emoji_Presentation # 6.1 [1] (😀) grinning face
|
||||
1F601..1F610 ; Emoji_Presentation # 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
|
||||
1F611 ; Emoji_Presentation # 6.1 [1] (😑) expressionless face
|
||||
1F612..1F614 ; Emoji_Presentation # 6.0 [3] (😒..😔) unamused face..pensive face
|
||||
1F615 ; Emoji_Presentation # 6.1 [1] (😕) confused face
|
||||
1F616 ; Emoji_Presentation # 6.0 [1] (😖) confounded face
|
||||
1F617 ; Emoji_Presentation # 6.1 [1] (😗) kissing face
|
||||
1F618 ; Emoji_Presentation # 6.0 [1] (😘) face blowing a kiss
|
||||
1F619 ; Emoji_Presentation # 6.1 [1] (😙) kissing face with smiling eyes
|
||||
1F61A ; Emoji_Presentation # 6.0 [1] (😚) kissing face with closed eyes
|
||||
1F61B ; Emoji_Presentation # 6.1 [1] (😛) face with tongue
|
||||
1F61C..1F61E ; Emoji_Presentation # 6.0 [3] (😜..😞) winking face with tongue..disappointed face
|
||||
1F61F ; Emoji_Presentation # 6.1 [1] (😟) worried face
|
||||
1F620..1F625 ; Emoji_Presentation # 6.0 [6] (😠..😥) angry face..sad but relieved face
|
||||
1F626..1F627 ; Emoji_Presentation # 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
|
||||
1F628..1F62B ; Emoji_Presentation # 6.0 [4] (😨..😫) fearful face..tired face
|
||||
1F62C ; Emoji_Presentation # 6.1 [1] (😬) grimacing face
|
||||
1F62D ; Emoji_Presentation # 6.0 [1] (😭) loudly crying face
|
||||
1F62E..1F62F ; Emoji_Presentation # 6.1 [2] (😮..😯) face with open mouth..hushed face
|
||||
1F630..1F633 ; Emoji_Presentation # 6.0 [4] (😰..😳) anxious face with sweat..flushed face
|
||||
1F634 ; Emoji_Presentation # 6.1 [1] (😴) sleeping face
|
||||
1F635..1F640 ; Emoji_Presentation # 6.0 [12] (😵..🙀) dizzy face..weary cat face
|
||||
1F641..1F642 ; Emoji_Presentation # 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
|
||||
1F643..1F644 ; Emoji_Presentation # 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
|
||||
1F645..1F64F ; Emoji_Presentation # 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
|
||||
1F680..1F6C5 ; Emoji_Presentation # 6.0 [70] (🚀..🛅) rocket..left luggage
|
||||
1F6CC ; Emoji_Presentation # 7.0 [1] (🛌) person in bed
|
||||
1F6D0 ; Emoji_Presentation # 8.0 [1] (🛐) place of worship
|
||||
1F6D1..1F6D2 ; Emoji_Presentation # 9.0 [2] (🛑..🛒) stop sign..shopping cart
|
||||
1F6EB..1F6EC ; Emoji_Presentation # 7.0 [2] (🛫..🛬) airplane departure..airplane arrival
|
||||
1F6F4..1F6F6 ; Emoji_Presentation # 9.0 [3] (🛴..🛶) kick scooter..canoe
|
||||
1F6F7..1F6F8 ; Emoji_Presentation # 10.0 [2] (🛷..🛸) sled..flying saucer
|
||||
1F910..1F918 ; Emoji_Presentation # 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
|
||||
1F919..1F91E ; Emoji_Presentation # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
|
||||
1F91F ; Emoji_Presentation # 10.0 [1] (🤟) love-you gesture
|
||||
1F920..1F927 ; Emoji_Presentation # 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
|
||||
1F928..1F92F ; Emoji_Presentation # 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
|
||||
1F930 ; Emoji_Presentation # 9.0 [1] (🤰) pregnant woman
|
||||
1F931..1F932 ; Emoji_Presentation # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
|
||||
1F933..1F93A ; Emoji_Presentation # 9.0 [8] (🤳..🤺) selfie..person fencing
|
||||
1F93C..1F93E ; Emoji_Presentation # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
|
||||
1F940..1F945 ; Emoji_Presentation # 9.0 [6] (🥀..🥅) wilted flower..goal net
|
||||
1F947..1F94B ; Emoji_Presentation # 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
|
||||
1F94C ; Emoji_Presentation # 10.0 [1] (🥌) curling stone
|
||||
1F950..1F95E ; Emoji_Presentation # 9.0 [15] (🥐..🥞) croissant..pancakes
|
||||
1F95F..1F96B ; Emoji_Presentation # 10.0 [13] (🥟..🥫) dumpling..canned food
|
||||
1F980..1F984 ; Emoji_Presentation # 8.0 [5] (🦀..🦄) crab..unicorn face
|
||||
1F985..1F991 ; Emoji_Presentation # 9.0 [13] (🦅..🦑) eagle..squid
|
||||
1F992..1F997 ; Emoji_Presentation # 10.0 [6] (🦒..🦗) giraffe..cricket
|
||||
1F9C0 ; Emoji_Presentation # 8.0 [1] (🧀) cheese wedge
|
||||
1F9D0..1F9E6 ; Emoji_Presentation # 10.0 [23] (🧐..🧦) face with monocle..socks
|
||||
|
||||
# Total elements: 966
|
||||
|
||||
# ================================================
|
||||
|
||||
# All omitted code points have Emoji_Modifier=No
|
||||
# @missing: 0000..10FFFF ; Emoji_Modifier ; No
|
||||
|
||||
1F3FB..1F3FF ; Emoji_Modifier # 8.0 [5] (🏻..🏿) light skin tone..dark skin tone
|
||||
|
||||
# Total elements: 5
|
||||
|
||||
# ================================================
|
||||
|
||||
# All omitted code points have Emoji_Modifier_Base=No
|
||||
# @missing: 0000..10FFFF ; Emoji_Modifier_Base ; No
|
||||
|
||||
261D ; Emoji_Modifier_Base # 1.1 [1] (☝️) index pointing up
|
||||
26F9 ; Emoji_Modifier_Base # 5.2 [1] (⛹️) person bouncing ball
|
||||
270A..270B ; Emoji_Modifier_Base # 6.0 [2] (✊..✋) raised fist..raised hand
|
||||
270C..270D ; Emoji_Modifier_Base # 1.1 [2] (✌️..✍️) victory hand..writing hand
|
||||
1F385 ; Emoji_Modifier_Base # 6.0 [1] (🎅) Santa Claus
|
||||
1F3C2..1F3C4 ; Emoji_Modifier_Base # 6.0 [3] (🏂..🏄) snowboarder..person surfing
|
||||
1F3C7 ; Emoji_Modifier_Base # 6.0 [1] (🏇) horse racing
|
||||
1F3CA ; Emoji_Modifier_Base # 6.0 [1] (🏊) person swimming
|
||||
1F3CB..1F3CC ; Emoji_Modifier_Base # 7.0 [2] (🏋️..🏌️) person lifting weights..person golfing
|
||||
1F442..1F443 ; Emoji_Modifier_Base # 6.0 [2] (👂..👃) ear..nose
|
||||
1F446..1F450 ; Emoji_Modifier_Base # 6.0 [11] (👆..👐) backhand index pointing up..open hands
|
||||
1F466..1F469 ; Emoji_Modifier_Base # 6.0 [4] (👦..👩) boy..woman
|
||||
1F46E ; Emoji_Modifier_Base # 6.0 [1] (👮) police officer
|
||||
1F470..1F478 ; Emoji_Modifier_Base # 6.0 [9] (👰..👸) bride with veil..princess
|
||||
1F47C ; Emoji_Modifier_Base # 6.0 [1] (👼) baby angel
|
||||
1F481..1F483 ; Emoji_Modifier_Base # 6.0 [3] (💁..💃) person tipping hand..woman dancing
|
||||
1F485..1F487 ; Emoji_Modifier_Base # 6.0 [3] (💅..💇) nail polish..person getting haircut
|
||||
1F4AA ; Emoji_Modifier_Base # 6.0 [1] (💪) flexed biceps
|
||||
1F574..1F575 ; Emoji_Modifier_Base # 7.0 [2] (🕴️..🕵️) man in suit levitating..detective
|
||||
1F57A ; Emoji_Modifier_Base # 9.0 [1] (🕺) man dancing
|
||||
1F590 ; Emoji_Modifier_Base # 7.0 [1] (🖐️) hand with fingers splayed
|
||||
1F595..1F596 ; Emoji_Modifier_Base # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
|
||||
1F645..1F647 ; Emoji_Modifier_Base # 6.0 [3] (🙅..🙇) person gesturing NO..person bowing
|
||||
1F64B..1F64F ; Emoji_Modifier_Base # 6.0 [5] (🙋..🙏) person raising hand..folded hands
|
||||
1F6A3 ; Emoji_Modifier_Base # 6.0 [1] (🚣) person rowing boat
|
||||
1F6B4..1F6B6 ; Emoji_Modifier_Base # 6.0 [3] (🚴..🚶) person biking..person walking
|
||||
1F6C0 ; Emoji_Modifier_Base # 6.0 [1] (🛀) person taking bath
|
||||
1F6CC ; Emoji_Modifier_Base # 7.0 [1] (🛌) person in bed
|
||||
1F918 ; Emoji_Modifier_Base # 8.0 [1] (🤘) sign of the horns
|
||||
1F919..1F91C ; Emoji_Modifier_Base # 9.0 [4] (🤙..🤜) call me hand..right-facing fist
|
||||
1F91E ; Emoji_Modifier_Base # 9.0 [1] (🤞) crossed fingers
|
||||
1F91F ; Emoji_Modifier_Base # 10.0 [1] (🤟) love-you gesture
|
||||
1F926 ; Emoji_Modifier_Base # 9.0 [1] (🤦) person facepalming
|
||||
1F930 ; Emoji_Modifier_Base # 9.0 [1] (🤰) pregnant woman
|
||||
1F931..1F932 ; Emoji_Modifier_Base # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
|
||||
1F933..1F939 ; Emoji_Modifier_Base # 9.0 [7] (🤳..🤹) selfie..person juggling
|
||||
1F93D..1F93E ; Emoji_Modifier_Base # 9.0 [2] (🤽..🤾) person playing water polo..person playing handball
|
||||
1F9D1..1F9DD ; Emoji_Modifier_Base # 10.0 [13] (🧑..🧝) adult..elf
|
||||
|
||||
# Total elements: 102
|
||||
|
||||
# ================================================
|
||||
|
||||
# All omitted code points have Emoji_Component=No
|
||||
# @missing: 0000..10FFFF ; Emoji_Component ; No
|
||||
|
||||
0023 ; Emoji_Component # 1.1 [1] (#️) number sign
|
||||
002A ; Emoji_Component # 1.1 [1] (*️) asterisk
|
||||
0030..0039 ; Emoji_Component # 1.1 [10] (0️..9️) digit zero..digit nine
|
||||
200D ; Emoji_Component # 1.1 [1] () zero width joiner
|
||||
20E3 ; Emoji_Component # 3.0 [1] (⃣) combining enclosing keycap
|
||||
FE0F ; Emoji_Component # 3.2 [1] () VARIATION SELECTOR-16
|
||||
1F1E6..1F1FF ; Emoji_Component # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
|
||||
1F3FB..1F3FF ; Emoji_Component # 8.0 [5] (🏻..🏿) light skin tone..dark skin tone
|
||||
E0020..E007F ; Emoji_Component # 3.1 [96] (..) tag space..cancel tag
|
||||
|
||||
# Total elements: 142
|
||||
|
||||
# ================================================
|
||||
|
||||
# All omitted code points have Extended_Pictographic=No
|
||||
# @missing: 0000..10FFFF ; Extended_Pictographic ; No
|
||||
|
||||
00A9 ; Extended_Pictographic# 1.1 [1] (©️) copyright
|
||||
00AE ; Extended_Pictographic# 1.1 [1] (®️) registered
|
||||
203C ; Extended_Pictographic# 1.1 [1] (‼️) double exclamation mark
|
||||
2049 ; Extended_Pictographic# 3.0 [1] (⁉️) exclamation question mark
|
||||
2122 ; Extended_Pictographic# 1.1 [1] (™️) trade mark
|
||||
2139 ; Extended_Pictographic# 3.0 [1] (ℹ️) information
|
||||
2194..2199 ; Extended_Pictographic# 1.1 [6] (↔️..↙️) left-right arrow..down-left arrow
|
||||
21A9..21AA ; Extended_Pictographic# 1.1 [2] (↩️..↪️) right arrow curving left..left arrow curving right
|
||||
231A..231B ; Extended_Pictographic# 1.1 [2] (⌚..⌛) watch..hourglass done
|
||||
2328 ; Extended_Pictographic# 1.1 [1] (⌨️) keyboard
|
||||
2388 ; Extended_Pictographic# 3.0 [1] (⎈️) HELM SYMBOL
|
||||
23CF ; Extended_Pictographic# 4.0 [1] (⏏️) eject button
|
||||
23E9..23F3 ; Extended_Pictographic# 6.0 [11] (⏩..⏳) fast-forward button..hourglass not done
|
||||
23F8..23FA ; Extended_Pictographic# 7.0 [3] (⏸️..⏺️) pause button..record button
|
||||
24C2 ; Extended_Pictographic# 1.1 [1] (Ⓜ️) circled M
|
||||
25AA..25AB ; Extended_Pictographic# 1.1 [2] (▪️..▫️) black small square..white small square
|
||||
25B6 ; Extended_Pictographic# 1.1 [1] (▶️) play button
|
||||
25C0 ; Extended_Pictographic# 1.1 [1] (◀️) reverse button
|
||||
25FB..25FE ; Extended_Pictographic# 3.2 [4] (◻️..◾) white medium square..black medium-small square
|
||||
2600..2605 ; Extended_Pictographic# 1.1 [6] (☀️..★️) sun..BLACK STAR
|
||||
2607..2612 ; Extended_Pictographic# 1.1 [12] (☇️..☒️) LIGHTNING..BALLOT BOX WITH X
|
||||
2614..2615 ; Extended_Pictographic# 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
|
||||
2616..2617 ; Extended_Pictographic# 3.2 [2] (☖️..☗️) WHITE SHOGI PIECE..BLACK SHOGI PIECE
|
||||
2618 ; Extended_Pictographic# 4.1 [1] (☘️) shamrock
|
||||
2619 ; Extended_Pictographic# 3.0 [1] (☙️) REVERSED ROTATED FLORAL HEART BULLET
|
||||
261A..266F ; Extended_Pictographic# 1.1 [86] (☚️..♯️) BLACK LEFT POINTING INDEX..MUSIC SHARP SIGN
|
||||
2670..2671 ; Extended_Pictographic# 3.0 [2] (♰️..♱️) WEST SYRIAC CROSS..EAST SYRIAC CROSS
|
||||
2672..267D ; Extended_Pictographic# 3.2 [12] (♲️..♽️) UNIVERSAL RECYCLING SYMBOL..PARTIALLY-RECYCLED PAPER SYMBOL
|
||||
267E..267F ; Extended_Pictographic# 4.1 [2] (♾️..♿) PERMANENT PAPER SIGN..wheelchair symbol
|
||||
2680..2689 ; Extended_Pictographic# 3.2 [10] (⚀️..⚉️) DIE FACE-1..BLACK CIRCLE WITH TWO WHITE DOTS
|
||||
268A..2691 ; Extended_Pictographic# 4.0 [8] (⚊️..⚑️) MONOGRAM FOR YANG..BLACK FLAG
|
||||
2692..269C ; Extended_Pictographic# 4.1 [11] (⚒️..⚜️) hammer and pick..fleur-de-lis
|
||||
269D ; Extended_Pictographic# 5.1 [1] (⚝️) OUTLINED WHITE STAR
|
||||
269E..269F ; Extended_Pictographic# 5.2 [2] (⚞️..⚟️) THREE LINES CONVERGING RIGHT..THREE LINES CONVERGING LEFT
|
||||
26A0..26A1 ; Extended_Pictographic# 4.0 [2] (⚠️..⚡) warning..high voltage
|
||||
26A2..26B1 ; Extended_Pictographic# 4.1 [16] (⚢️..⚱️) DOUBLED FEMALE SIGN..funeral urn
|
||||
26B2 ; Extended_Pictographic# 5.0 [1] (⚲️) NEUTER
|
||||
26B3..26BC ; Extended_Pictographic# 5.1 [10] (⚳️..⚼️) CERES..SESQUIQUADRATE
|
||||
26BD..26BF ; Extended_Pictographic# 5.2 [3] (⚽..⚿️) soccer ball..SQUARED KEY
|
||||
26C0..26C3 ; Extended_Pictographic# 5.1 [4] (⛀️..⛃️) WHITE DRAUGHTS MAN..BLACK DRAUGHTS KING
|
||||
26C4..26CD ; Extended_Pictographic# 5.2 [10] (⛄..⛍️) snowman without snow..DISABLED CAR
|
||||
26CE ; Extended_Pictographic# 6.0 [1] (⛎) Ophiuchus
|
||||
26CF..26E1 ; Extended_Pictographic# 5.2 [19] (⛏️..⛡️) pick..RESTRICTED LEFT ENTRY-2
|
||||
26E2 ; Extended_Pictographic# 6.0 [1] (⛢️) ASTRONOMICAL SYMBOL FOR URANUS
|
||||
26E3 ; Extended_Pictographic# 5.2 [1] (⛣️) HEAVY CIRCLE WITH STROKE AND TWO DOTS ABOVE
|
||||
26E4..26E7 ; Extended_Pictographic# 6.0 [4] (⛤️..⛧️) PENTAGRAM..INVERTED PENTAGRAM
|
||||
26E8..26FF ; Extended_Pictographic# 5.2 [24] (⛨️..⛿️) BLACK CROSS ON SHIELD..WHITE FLAG WITH HORIZONTAL MIDDLE BLACK STRIPE
|
||||
2700 ; Extended_Pictographic# 7.0 [1] (✀️) BLACK SAFETY SCISSORS
|
||||
2701..2704 ; Extended_Pictographic# 1.1 [4] (✁️..✄️) UPPER BLADE SCISSORS..WHITE SCISSORS
|
||||
2705 ; Extended_Pictographic# 6.0 [1] (✅) white heavy check mark
|
||||
2708..2709 ; Extended_Pictographic# 1.1 [2] (✈️..✉️) airplane..envelope
|
||||
270A..270B ; Extended_Pictographic# 6.0 [2] (✊..✋) raised fist..raised hand
|
||||
270C..2712 ; Extended_Pictographic# 1.1 [7] (✌️..✒️) victory hand..black nib
|
||||
2714 ; Extended_Pictographic# 1.1 [1] (✔️) heavy check mark
|
||||
2716 ; Extended_Pictographic# 1.1 [1] (✖️) heavy multiplication x
|
||||
271D ; Extended_Pictographic# 1.1 [1] (✝️) latin cross
|
||||
2721 ; Extended_Pictographic# 1.1 [1] (✡️) star of David
|
||||
2728 ; Extended_Pictographic# 6.0 [1] (✨) sparkles
|
||||
2733..2734 ; Extended_Pictographic# 1.1 [2] (✳️..✴️) eight-spoked asterisk..eight-pointed star
|
||||
2744 ; Extended_Pictographic# 1.1 [1] (❄️) snowflake
|
||||
2747 ; Extended_Pictographic# 1.1 [1] (❇️) sparkle
|
||||
274C ; Extended_Pictographic# 6.0 [1] (❌) cross mark
|
||||
274E ; Extended_Pictographic# 6.0 [1] (❎) cross mark button
|
||||
2753..2755 ; Extended_Pictographic# 6.0 [3] (❓..❕) question mark..white exclamation mark
|
||||
2757 ; Extended_Pictographic# 5.2 [1] (❗) exclamation mark
|
||||
2763..2767 ; Extended_Pictographic# 1.1 [5] (❣️..❧️) heavy heart exclamation..ROTATED FLORAL HEART BULLET
|
||||
2795..2797 ; Extended_Pictographic# 6.0 [3] (➕..➗) heavy plus sign..heavy division sign
|
||||
27A1 ; Extended_Pictographic# 1.1 [1] (➡️) right arrow
|
||||
27B0 ; Extended_Pictographic# 6.0 [1] (➰) curly loop
|
||||
27BF ; Extended_Pictographic# 6.0 [1] (➿) double curly loop
|
||||
2934..2935 ; Extended_Pictographic# 3.2 [2] (⤴️..⤵️) right arrow curving up..right arrow curving down
|
||||
2B05..2B07 ; Extended_Pictographic# 4.0 [3] (⬅️..⬇️) left arrow..down arrow
|
||||
2B1B..2B1C ; Extended_Pictographic# 5.1 [2] (⬛..⬜) black large square..white large square
|
||||
2B50 ; Extended_Pictographic# 5.1 [1] (⭐) white medium star
|
||||
2B55 ; Extended_Pictographic# 5.2 [1] (⭕) heavy large circle
|
||||
3030 ; Extended_Pictographic# 1.1 [1] (〰️) wavy dash
|
||||
303D ; Extended_Pictographic# 3.2 [1] (〽️) part alternation mark
|
||||
3297 ; Extended_Pictographic# 1.1 [1] (㊗️) Japanese “congratulations” button
|
||||
3299 ; Extended_Pictographic# 1.1 [1] (㊙️) Japanese “secret” button
|
||||
1F000..1F02B ; Extended_Pictographic# 5.1 [44] (🀀️..🀫️) MAHJONG TILE EAST WIND..MAHJONG TILE BACK
|
||||
1F02C..1F02F ; Extended_Pictographic# 10.0 [4] (️..️) <reserved-1F02C>..<reserved-1F02F>
|
||||
1F030..1F093 ; Extended_Pictographic# 5.1[100] (🀰️..🂓️) DOMINO TILE HORIZONTAL BACK..DOMINO TILE VERTICAL-06-06
|
||||
1F094..1F09F ; Extended_Pictographic# 10.0 [12] (️..️) <reserved-1F094>..<reserved-1F09F>
|
||||
1F0A0..1F0AE ; Extended_Pictographic# 6.0 [15] (🂠️..🂮️) PLAYING CARD BACK..PLAYING CARD KING OF SPADES
|
||||
1F0AF..1F0B0 ; Extended_Pictographic# 10.0 [2] (️..️) <reserved-1F0AF>..<reserved-1F0B0>
|
||||
1F0B1..1F0BE ; Extended_Pictographic# 6.0 [14] (🂱️..🂾️) PLAYING CARD ACE OF HEARTS..PLAYING CARD KING OF HEARTS
|
||||
1F0BF ; Extended_Pictographic# 7.0 [1] (🂿️) PLAYING CARD RED JOKER
|
||||
1F0C0 ; Extended_Pictographic# 10.0 [1] (️) <reserved-1F0C0>
|
||||
1F0C1..1F0CF ; Extended_Pictographic# 6.0 [15] (🃁️..🃏) PLAYING CARD ACE OF DIAMONDS..joker
|
||||
1F0D0 ; Extended_Pictographic# 10.0 [1] (️) <reserved-1F0D0>
|
||||
1F0D1..1F0DF ; Extended_Pictographic# 6.0 [15] (🃑️..🃟️) PLAYING CARD ACE OF CLUBS..PLAYING CARD WHITE JOKER
|
||||
1F0E0..1F0F5 ; Extended_Pictographic# 7.0 [22] (🃠️..🃵️) PLAYING CARD FOOL..PLAYING CARD TRUMP-21
|
||||
1F0F6..1F0FF ; Extended_Pictographic# 10.0 [10] (️..️) <reserved-1F0F6>..<reserved-1F0FF>
|
||||
1F10D..1F10F ; Extended_Pictographic# 10.0 [3] (🄍️..🄏️) <reserved-1F10D>..<reserved-1F10F>
|
||||
1F12F ; Extended_Pictographic# 10.0 [1] (🄯️) <reserved-1F12F>
|
||||
1F16C..1F16F ; Extended_Pictographic# 10.0 [4] (🅬️..🅯️) <reserved-1F16C>..<reserved-1F16F>
|
||||
1F170..1F171 ; Extended_Pictographic# 6.0 [2] (🅰️..🅱️) A button (blood type)..B button (blood type)
|
||||
1F17E ; Extended_Pictographic# 6.0 [1] (🅾️) O button (blood type)
|
||||
1F17F ; Extended_Pictographic# 5.2 [1] (🅿️) P button
|
||||
1F18E ; Extended_Pictographic# 6.0 [1] (🆎) AB button (blood type)
|
||||
1F191..1F19A ; Extended_Pictographic# 6.0 [10] (🆑..🆚) CL button..VS button
|
||||
1F1AD..1F1E5 ; Extended_Pictographic# 10.0 [57] (🆭️..️) <reserved-1F1AD>..<reserved-1F1E5>
|
||||
1F201..1F202 ; Extended_Pictographic# 6.0 [2] (🈁..🈂️) Japanese “here” button..Japanese “service charge” button
|
||||
1F203..1F20F ; Extended_Pictographic# 10.0 [13] (️..️) <reserved-1F203>..<reserved-1F20F>
|
||||
1F21A ; Extended_Pictographic# 5.2 [1] (🈚) Japanese “free of charge” button
|
||||
1F22F ; Extended_Pictographic# 5.2 [1] (🈯) Japanese “reserved” button
|
||||
1F232..1F23A ; Extended_Pictographic# 6.0 [9] (🈲..🈺) Japanese “prohibited” button..Japanese “open for business” button
|
||||
1F23C..1F23F ; Extended_Pictographic# 10.0 [4] (️..️) <reserved-1F23C>..<reserved-1F23F>
|
||||
1F249..1F24F ; Extended_Pictographic# 10.0 [7] (️..️) <reserved-1F249>..<reserved-1F24F>
|
||||
1F250..1F251 ; Extended_Pictographic# 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
|
||||
1F252..1F25F ; Extended_Pictographic# 10.0 [14] (️..️) <reserved-1F252>..<reserved-1F25F>
|
||||
1F260..1F265 ; Extended_Pictographic# 10.0 [6] (🉠️..🉥️) ROUNDED SYMBOL FOR FU..ROUNDED SYMBOL FOR CAI
|
||||
1F266..1F2FF ; Extended_Pictographic# 10.0[154] (️..️) <reserved-1F266>..<reserved-1F2FF>
|
||||
1F300..1F320 ; Extended_Pictographic# 6.0 [33] (🌀..🌠) cyclone..shooting star
|
||||
1F321..1F32C ; Extended_Pictographic# 7.0 [12] (🌡️..🌬️) thermometer..wind face
|
||||
1F32D..1F32F ; Extended_Pictographic# 8.0 [3] (🌭..🌯) hot dog..burrito
|
||||
1F330..1F335 ; Extended_Pictographic# 6.0 [6] (🌰..🌵) chestnut..cactus
|
||||
1F336 ; Extended_Pictographic# 7.0 [1] (🌶️) hot pepper
|
||||
1F337..1F37C ; Extended_Pictographic# 6.0 [70] (🌷..🍼) tulip..baby bottle
|
||||
1F37D ; Extended_Pictographic# 7.0 [1] (🍽️) fork and knife with plate
|
||||
1F37E..1F37F ; Extended_Pictographic# 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
|
||||
1F380..1F393 ; Extended_Pictographic# 6.0 [20] (🎀..🎓) ribbon..graduation cap
|
||||
1F394..1F39F ; Extended_Pictographic# 7.0 [12] (🎔️..🎟️) HEART WITH TIP ON THE LEFT..admission tickets
|
||||
1F3A0..1F3C4 ; Extended_Pictographic# 6.0 [37] (🎠..🏄) carousel horse..person surfing
|
||||
1F3C5 ; Extended_Pictographic# 7.0 [1] (🏅) sports medal
|
||||
1F3C6..1F3CA ; Extended_Pictographic# 6.0 [5] (🏆..🏊) trophy..person swimming
|
||||
1F3CB..1F3CE ; Extended_Pictographic# 7.0 [4] (🏋️..🏎️) person lifting weights..racing car
|
||||
1F3CF..1F3D3 ; Extended_Pictographic# 8.0 [5] (🏏..🏓) cricket game..ping pong
|
||||
1F3D4..1F3DF ; Extended_Pictographic# 7.0 [12] (🏔️..🏟️) snow-capped mountain..stadium
|
||||
1F3E0..1F3F0 ; Extended_Pictographic# 6.0 [17] (🏠..🏰) house..castle
|
||||
1F3F1..1F3F7 ; Extended_Pictographic# 7.0 [7] (🏱️..🏷️) WHITE PENNANT..label
|
||||
1F3F8..1F3FA ; Extended_Pictographic# 8.0 [3] (🏸..🏺) badminton..amphora
|
||||
1F400..1F43E ; Extended_Pictographic# 6.0 [63] (🐀..🐾) rat..paw prints
|
||||
1F43F ; Extended_Pictographic# 7.0 [1] (🐿️) chipmunk
|
||||
1F440 ; Extended_Pictographic# 6.0 [1] (👀) eyes
|
||||
1F441 ; Extended_Pictographic# 7.0 [1] (👁️) eye
|
||||
1F442..1F4F7 ; Extended_Pictographic# 6.0[182] (👂..📷) ear..camera
|
||||
1F4F8 ; Extended_Pictographic# 7.0 [1] (📸) camera with flash
|
||||
1F4F9..1F4FC ; Extended_Pictographic# 6.0 [4] (📹..📼) video camera..videocassette
|
||||
1F4FD..1F4FE ; Extended_Pictographic# 7.0 [2] (📽️..📾️) film projector..PORTABLE STEREO
|
||||
1F4FF ; Extended_Pictographic# 8.0 [1] (📿) prayer beads
|
||||
1F500..1F53D ; Extended_Pictographic# 6.0 [62] (🔀..🔽) shuffle tracks button..down button
|
||||
1F53E..1F53F ; Extended_Pictographic# 7.0 [2] (🔾️..🔿️) LOWER RIGHT SHADOWED WHITE CIRCLE..UPPER RIGHT SHADOWED WHITE CIRCLE
|
||||
1F540..1F543 ; Extended_Pictographic# 6.1 [4] (🕀️..🕃️) CIRCLED CROSS POMMEE..NOTCHED LEFT SEMICIRCLE WITH THREE DOTS
|
||||
1F544..1F54A ; Extended_Pictographic# 7.0 [7] (🕄️..🕊️) NOTCHED RIGHT SEMICIRCLE WITH THREE DOTS..dove
|
||||
1F54B..1F54F ; Extended_Pictographic# 8.0 [5] (🕋..🕏️) kaaba..BOWL OF HYGIEIA
|
||||
1F550..1F567 ; Extended_Pictographic# 6.0 [24] (🕐..🕧) one o’clock..twelve-thirty
|
||||
1F568..1F579 ; Extended_Pictographic# 7.0 [18] (🕨️..🕹️) RIGHT SPEAKER..joystick
|
||||
1F57A ; Extended_Pictographic# 9.0 [1] (🕺) man dancing
|
||||
1F57B..1F5A3 ; Extended_Pictographic# 7.0 [41] (🕻️..🖣️) LEFT HAND TELEPHONE RECEIVER..BLACK DOWN POINTING BACKHAND INDEX
|
||||
1F5A4 ; Extended_Pictographic# 9.0 [1] (🖤) black heart
|
||||
1F5A5..1F5FA ; Extended_Pictographic# 7.0 [86] (🖥️..🗺️) desktop computer..world map
|
||||
1F5FB..1F5FF ; Extended_Pictographic# 6.0 [5] (🗻..🗿) mount fuji..moai
|
||||
1F600 ; Extended_Pictographic# 6.1 [1] (😀) grinning face
|
||||
1F601..1F610 ; Extended_Pictographic# 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
|
||||
1F611 ; Extended_Pictographic# 6.1 [1] (😑) expressionless face
|
||||
1F612..1F614 ; Extended_Pictographic# 6.0 [3] (😒..😔) unamused face..pensive face
|
||||
1F615 ; Extended_Pictographic# 6.1 [1] (😕) confused face
|
||||
1F616 ; Extended_Pictographic# 6.0 [1] (😖) confounded face
|
||||
1F617 ; Extended_Pictographic# 6.1 [1] (😗) kissing face
|
||||
1F618 ; Extended_Pictographic# 6.0 [1] (😘) face blowing a kiss
|
||||
1F619 ; Extended_Pictographic# 6.1 [1] (😙) kissing face with smiling eyes
|
||||
1F61A ; Extended_Pictographic# 6.0 [1] (😚) kissing face with closed eyes
|
||||
1F61B ; Extended_Pictographic# 6.1 [1] (😛) face with tongue
|
||||
1F61C..1F61E ; Extended_Pictographic# 6.0 [3] (😜..😞) winking face with tongue..disappointed face
|
||||
1F61F ; Extended_Pictographic# 6.1 [1] (😟) worried face
|
||||
1F620..1F625 ; Extended_Pictographic# 6.0 [6] (😠..😥) angry face..sad but relieved face
|
||||
1F626..1F627 ; Extended_Pictographic# 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
|
||||
1F628..1F62B ; Extended_Pictographic# 6.0 [4] (😨..😫) fearful face..tired face
|
||||
1F62C ; Extended_Pictographic# 6.1 [1] (😬) grimacing face
|
||||
1F62D ; Extended_Pictographic# 6.0 [1] (😭) loudly crying face
|
||||
1F62E..1F62F ; Extended_Pictographic# 6.1 [2] (😮..😯) face with open mouth..hushed face
|
||||
1F630..1F633 ; Extended_Pictographic# 6.0 [4] (😰..😳) anxious face with sweat..flushed face
|
||||
1F634 ; Extended_Pictographic# 6.1 [1] (😴) sleeping face
|
||||
1F635..1F640 ; Extended_Pictographic# 6.0 [12] (😵..🙀) dizzy face..weary cat face
|
||||
1F641..1F642 ; Extended_Pictographic# 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
|
||||
1F643..1F644 ; Extended_Pictographic# 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
|
||||
1F645..1F64F ; Extended_Pictographic# 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
|
||||
1F680..1F6C5 ; Extended_Pictographic# 6.0 [70] (🚀..🛅) rocket..left luggage
|
||||
1F6C6..1F6CF ; Extended_Pictographic# 7.0 [10] (🛆️..🛏️) TRIANGLE WITH ROUNDED CORNERS..bed
|
||||
1F6D0 ; Extended_Pictographic# 8.0 [1] (🛐) place of worship
|
||||
1F6D1..1F6D2 ; Extended_Pictographic# 9.0 [2] (🛑..🛒) stop sign..shopping cart
|
||||
1F6D3..1F6D4 ; Extended_Pictographic# 10.0 [2] (🛓️..🛔️) STUPA..PAGODA
|
||||
1F6D5..1F6DF ; Extended_Pictographic# 10.0 [11] (🛕️..🛟️) <reserved-1F6D5>..<reserved-1F6DF>
|
||||
1F6E0..1F6EC ; Extended_Pictographic# 7.0 [13] (🛠️..🛬) hammer and wrench..airplane arrival
|
||||
1F6ED..1F6EF ; Extended_Pictographic# 10.0 [3] (️..️) <reserved-1F6ED>..<reserved-1F6EF>
|
||||
1F6F0..1F6F3 ; Extended_Pictographic# 7.0 [4] (🛰️..🛳️) satellite..passenger ship
|
||||
1F6F4..1F6F6 ; Extended_Pictographic# 9.0 [3] (🛴..🛶) kick scooter..canoe
|
||||
1F6F7..1F6F8 ; Extended_Pictographic# 10.0 [2] (🛷..🛸) sled..flying saucer
|
||||
1F6F9..1F6FF ; Extended_Pictographic# 10.0 [7] (🛹️..️) <reserved-1F6F9>..<reserved-1F6FF>
|
||||
1F774..1F77F ; Extended_Pictographic# 10.0 [12] (🝴️..🝿️) <reserved-1F774>..<reserved-1F77F>
|
||||
1F7D5..1F7FF ; Extended_Pictographic# 10.0 [43] (🟕️..️) <reserved-1F7D5>..<reserved-1F7FF>
|
||||
1F80C..1F80F ; Extended_Pictographic# 10.0 [4] (️..️) <reserved-1F80C>..<reserved-1F80F>
|
||||
1F848..1F84F ; Extended_Pictographic# 10.0 [8] (️..️) <reserved-1F848>..<reserved-1F84F>
|
||||
1F85A..1F85F ; Extended_Pictographic# 10.0 [6] (️..️) <reserved-1F85A>..<reserved-1F85F>
|
||||
1F888..1F88F ; Extended_Pictographic# 10.0 [8] (️..️) <reserved-1F888>..<reserved-1F88F>
|
||||
1F8AE..1F8FF ; Extended_Pictographic# 10.0 [82] (️..️) <reserved-1F8AE>..<reserved-1F8FF>
|
||||
1F900..1F90B ; Extended_Pictographic# 10.0 [12] (🤀️..🤋️) CIRCLED CROSS FORMEE WITH FOUR DOTS..DOWNWARD FACING NOTCHED HOOK WITH DOT
|
||||
1F90C..1F90F ; Extended_Pictographic# 10.0 [4] (🤌️..🤏️) <reserved-1F90C>..<reserved-1F90F>
|
||||
1F910..1F918 ; Extended_Pictographic# 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
|
||||
1F919..1F91E ; Extended_Pictographic# 9.0 [6] (🤙..🤞) call me hand..crossed fingers
|
||||
1F91F ; Extended_Pictographic# 10.0 [1] (🤟) love-you gesture
|
||||
1F920..1F927 ; Extended_Pictographic# 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
|
||||
1F928..1F92F ; Extended_Pictographic# 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
|
||||
1F930 ; Extended_Pictographic# 9.0 [1] (🤰) pregnant woman
|
||||
1F931..1F932 ; Extended_Pictographic# 10.0 [2] (🤱..🤲) breast-feeding..palms up together
|
||||
1F933..1F93A ; Extended_Pictographic# 9.0 [8] (🤳..🤺) selfie..person fencing
|
||||
1F93C..1F93E ; Extended_Pictographic# 9.0 [3] (🤼..🤾) people wrestling..person playing handball
|
||||
1F93F ; Extended_Pictographic# 10.0 [1] (🤿️) <reserved-1F93F>
|
||||
1F940..1F945 ; Extended_Pictographic# 9.0 [6] (🥀..🥅) wilted flower..goal net
|
||||
1F947..1F94B ; Extended_Pictographic# 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
|
||||
1F94C ; Extended_Pictographic# 10.0 [1] (🥌) curling stone
|
||||
1F94D..1F94F ; Extended_Pictographic# 10.0 [3] (🥍️..🥏️) <reserved-1F94D>..<reserved-1F94F>
|
||||
1F950..1F95E ; Extended_Pictographic# 9.0 [15] (🥐..🥞) croissant..pancakes
|
||||
1F95F..1F96B ; Extended_Pictographic# 10.0 [13] (🥟..🥫) dumpling..canned food
|
||||
1F96C..1F97F ; Extended_Pictographic# 10.0 [20] (🥬️..🥿️) <reserved-1F96C>..<reserved-1F97F>
|
||||
1F980..1F984 ; Extended_Pictographic# 8.0 [5] (🦀..🦄) crab..unicorn face
|
||||
1F985..1F991 ; Extended_Pictographic# 9.0 [13] (🦅..🦑) eagle..squid
|
||||
1F992..1F997 ; Extended_Pictographic# 10.0 [6] (🦒..🦗) giraffe..cricket
|
||||
1F998..1F9BF ; Extended_Pictographic# 10.0 [40] (🦘️..🦿️) <reserved-1F998>..<reserved-1F9BF>
|
||||
1F9C0 ; Extended_Pictographic# 8.0 [1] (🧀) cheese wedge
|
||||
1F9C1..1F9CF ; Extended_Pictographic# 10.0 [15] (🧁️..🧏️) <reserved-1F9C1>..<reserved-1F9CF>
|
||||
1F9D0..1F9E6 ; Extended_Pictographic# 10.0 [23] (🧐..🧦) face with monocle..socks
|
||||
1F9E7..1FFFD ; Extended_Pictographic# 10.0[1559] (🧧️..️) <reserved-1F9E7>..<reserved-1FFFD>
|
||||
|
||||
# Total elements: 3823
|
||||
|
||||
#EOF
|
|
@ -1,88 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import commands
|
||||
import subprocess
|
||||
import io
|
||||
|
||||
def fail(message):
|
||||
print message
|
||||
sys.exit(1)
|
||||
|
||||
# For simplicity and compactness, we pre-define the
|
||||
# emoji code planes to ensure that all of the currently-used
|
||||
# emoji ranges within them are combined.
|
||||
big_ranges = [
|
||||
(0x1F600,0x1F64F, ),
|
||||
(0x1F300,0x1F5FF, ),
|
||||
(0x1F680,0x1F6FF, ),
|
||||
(0x2600,0x26FF, ),
|
||||
(0x2700,0x27BF, ),
|
||||
(0xFE00,0xFE0F, ),
|
||||
(0x1F900,0x1F9FF, ),
|
||||
(65024,65039,),
|
||||
(8400, 8447,)
|
||||
]
|
||||
|
||||
if __name__ == '__main__':
|
||||
src_filename = "emoji-data.txt"
|
||||
src_dir_path = os.path.dirname(__file__)
|
||||
src_file_path = os.path.join(src_dir_path, src_filename)
|
||||
print 'src_file_path', src_file_path
|
||||
if not os.path.exists(src_file_path):
|
||||
fail("Could not find input file")
|
||||
|
||||
with io.open(src_file_path, "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
|
||||
lines = text.split('\n')
|
||||
raw_ranges = []
|
||||
for line in lines:
|
||||
if '#' in line:
|
||||
line = line[:line.index('#')].strip()
|
||||
if ';' not in line:
|
||||
continue
|
||||
print 'line:', line
|
||||
range_text = line[:line.index(';')]
|
||||
print '\t:', range_text
|
||||
if '..' in range_text:
|
||||
range_start_hex_string, range_end_hex_string = range_text.split('..')
|
||||
else:
|
||||
range_start_hex_string = range_end_hex_string = range_text.strip()
|
||||
range_start = int(range_start_hex_string.strip(), 16)
|
||||
range_end = int(range_end_hex_string.strip(), 16)
|
||||
print '\t', range_start, range_end
|
||||
|
||||
raw_ranges.append((range_start, range_end,))
|
||||
|
||||
raw_ranges += big_ranges
|
||||
|
||||
raw_ranges.sort(key=lambda a:a[0])
|
||||
|
||||
|
||||
new_ranges = []
|
||||
for range_start, range_end in raw_ranges:
|
||||
if len(new_ranges) > 0:
|
||||
last_range = new_ranges[-1]
|
||||
# print 'last_range', last_range
|
||||
last_range_start, last_range_end = last_range
|
||||
if range_start >= last_range_start and range_start <= last_range_end + 1:
|
||||
# if last_range_end + 1 == range_start:
|
||||
new_ranges = new_ranges[:-1]
|
||||
print 'merging', last_range_start, last_range_end, 'and', range_start, range_end
|
||||
new_ranges.append((last_range_start, max(range_end, last_range_end),))
|
||||
continue
|
||||
|
||||
new_ranges.append((range_start, range_end,))
|
||||
|
||||
print
|
||||
for range_start, range_end in new_ranges:
|
||||
# print '0x%X...0x%X, // %d Emotions' % (range_start, range_end, (1 + range_end - range_start), )
|
||||
print 'EmojiRange(rangeStart:0x%X, rangeEnd:0x%X),' % (range_start, range_end, )
|
||||
print 'new_ranges:', len(new_ranges)
|
||||
print
|
||||
print 'Copy and paste the code above into DisplayableText.swift'
|
||||
print
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
Copy these git hooks into .git/hooks
|
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
Scripts/reverse_integration_check.py
|
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
Scripts/precommit.py
|
|
@ -1,475 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import datetime
|
||||
import argparse
|
||||
import commands
|
||||
|
||||
|
||||
git_repo_path = os.path.abspath(subprocess.check_output(['git', 'rev-parse', '--show-toplevel']).strip())
|
||||
|
||||
|
||||
|
||||
class include:
|
||||
def __init__(self, isInclude, isQuote, body, comment):
|
||||
self.isInclude = isInclude
|
||||
self.isQuote = isQuote
|
||||
self.body = body
|
||||
self.comment = comment
|
||||
|
||||
def format(self):
|
||||
result = '%s %s%s%s' % (
|
||||
('#include' if self.isInclude else '#import'),
|
||||
('"' if self.isQuote else '<'),
|
||||
self.body.strip(),
|
||||
('"' if self.isQuote else '>'),
|
||||
)
|
||||
if self.comment.strip():
|
||||
result += ' ' + self.comment.strip()
|
||||
return result
|
||||
|
||||
|
||||
def is_include_or_import(line):
|
||||
line = line.strip()
|
||||
if line.startswith('#include '):
|
||||
return True
|
||||
elif line.startswith('#import '):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def parse_include(line):
|
||||
remainder = line.strip()
|
||||
|
||||
if remainder.startswith('#include '):
|
||||
isInclude = True
|
||||
remainder = remainder[len('#include '):]
|
||||
elif remainder.startswith('#import '):
|
||||
isInclude = False
|
||||
remainder = remainder[len('#import '):]
|
||||
elif remainder == '//':
|
||||
return None
|
||||
elif not remainder:
|
||||
return None
|
||||
else:
|
||||
print ('Unexpected import or include: '+ line)
|
||||
sys.exit(1)
|
||||
|
||||
comment = None
|
||||
if remainder.startswith('"'):
|
||||
isQuote = True
|
||||
endIndex = remainder.find('"', 1)
|
||||
if endIndex < 0:
|
||||
print ('Unexpected import or include: '+ line)
|
||||
sys.exit(1)
|
||||
body = remainder[1:endIndex]
|
||||
comment = remainder[endIndex+1:]
|
||||
elif remainder.startswith('<'):
|
||||
isQuote = False
|
||||
endIndex = remainder.find('>', 1)
|
||||
if endIndex < 0:
|
||||
print ('Unexpected import or include: '+ line)
|
||||
sys.exit(1)
|
||||
body = remainder[1:endIndex]
|
||||
comment = remainder[endIndex+1:]
|
||||
else:
|
||||
print ('Unexpected import or include: '+ remainder)
|
||||
sys.exit(1)
|
||||
|
||||
return include(isInclude, isQuote, body, comment)
|
||||
|
||||
|
||||
def parse_includes(text):
|
||||
lines = text.split('\n')
|
||||
|
||||
includes = []
|
||||
for line in lines:
|
||||
include = parse_include(line)
|
||||
if include:
|
||||
includes.append(include)
|
||||
|
||||
return includes
|
||||
|
||||
|
||||
def sort_include_block(text, filepath, filename, file_extension):
|
||||
lines = text.split('\n')
|
||||
|
||||
includes = parse_includes(text)
|
||||
|
||||
blocks = []
|
||||
|
||||
file_extension = file_extension.lower()
|
||||
|
||||
for include in includes:
|
||||
include.isInclude = False
|
||||
|
||||
if file_extension in ('c', 'cpp', 'hpp'):
|
||||
for include in includes:
|
||||
include.isInclude = True
|
||||
elif file_extension in ('m'):
|
||||
for include in includes:
|
||||
include.isInclude = False
|
||||
|
||||
# Make sure matching header is first.
|
||||
matching_header_includes = []
|
||||
other_includes = []
|
||||
def is_matching_header(include):
|
||||
filename_wo_ext = os.path.splitext(filename)[0]
|
||||
include_filename_wo_ext = os.path.splitext(os.path.basename(include.body))[0]
|
||||
return filename_wo_ext == include_filename_wo_ext
|
||||
for include in includes:
|
||||
if is_matching_header(include):
|
||||
matching_header_includes.append(include)
|
||||
else:
|
||||
other_includes.append(include)
|
||||
includes = other_includes
|
||||
|
||||
def formatBlock(includes):
|
||||
lines = [include.format() for include in includes]
|
||||
lines = list(set(lines))
|
||||
def include_sorter(a, b):
|
||||
# return cmp(a.lower(), b.lower())
|
||||
return cmp(a, b)
|
||||
# print 'before'
|
||||
# for line in lines:
|
||||
# print '\t', line
|
||||
# print
|
||||
lines.sort(include_sorter)
|
||||
# print 'after'
|
||||
# for line in lines:
|
||||
# print '\t', line
|
||||
# print
|
||||
# print
|
||||
# print 'filepath'
|
||||
# for line in lines:
|
||||
# print '\t', line
|
||||
# print
|
||||
return '\n'.join(lines)
|
||||
|
||||
includeAngles = [include for include in includes if include.isInclude and not include.isQuote]
|
||||
includeQuotes = [include for include in includes if include.isInclude and include.isQuote]
|
||||
importAngles = [include for include in includes if (not include.isInclude) and not include.isQuote]
|
||||
importQuotes = [include for include in includes if (not include.isInclude) and include.isQuote]
|
||||
if matching_header_includes:
|
||||
blocks.append(formatBlock(matching_header_includes))
|
||||
if includeQuotes:
|
||||
blocks.append(formatBlock(includeQuotes))
|
||||
if includeAngles:
|
||||
blocks.append(formatBlock(includeAngles))
|
||||
if importQuotes:
|
||||
blocks.append(formatBlock(importQuotes))
|
||||
if importAngles:
|
||||
blocks.append(formatBlock(importAngles))
|
||||
|
||||
return '\n'.join(blocks) + '\n'
|
||||
|
||||
|
||||
def sort_class_statement_block(text, filepath, filename, file_extension):
|
||||
lines = text.split('\n')
|
||||
lines = [line.strip() for line in lines if line.strip()]
|
||||
lines = list(set(lines))
|
||||
lines.sort()
|
||||
return '\n' + '\n'.join(lines) + '\n'
|
||||
|
||||
|
||||
def find_matching_section(text, match_test):
|
||||
lines = text.split('\n')
|
||||
first_matching_line_index = None
|
||||
for index, line in enumerate(lines):
|
||||
if match_test(line):
|
||||
first_matching_line_index = index
|
||||
break
|
||||
|
||||
if first_matching_line_index is None:
|
||||
return None
|
||||
|
||||
# Absorb any leading empty lines.
|
||||
while first_matching_line_index > 0:
|
||||
prev_line = lines[first_matching_line_index - 1]
|
||||
if prev_line.strip():
|
||||
break
|
||||
first_matching_line_index = first_matching_line_index - 1
|
||||
|
||||
first_non_matching_line_index = None
|
||||
for index, line in enumerate(lines[first_matching_line_index:]):
|
||||
if not line.strip():
|
||||
# Absorb any trailing empty lines.
|
||||
continue
|
||||
if not match_test(line):
|
||||
first_non_matching_line_index = index + first_matching_line_index
|
||||
break
|
||||
|
||||
text0 = '\n'.join(lines[:first_matching_line_index])
|
||||
if first_non_matching_line_index is None:
|
||||
text1 = '\n'.join(lines[first_matching_line_index:])
|
||||
text2 = None
|
||||
else:
|
||||
text1 = '\n'.join(lines[first_matching_line_index:first_non_matching_line_index])
|
||||
text2 = '\n'.join(lines[first_non_matching_line_index:])
|
||||
|
||||
return text0, text1, text2
|
||||
|
||||
|
||||
def sort_matching_blocks(sort_name, filepath, filename, file_extension, text, match_func, sort_func):
|
||||
unprocessed = text
|
||||
processed = None
|
||||
while True:
|
||||
section = find_matching_section(unprocessed, match_func)
|
||||
# print '\t', 'sort_matching_blocks', section
|
||||
if not section:
|
||||
if processed:
|
||||
processed = '\n'.join((processed, unprocessed,))
|
||||
else:
|
||||
processed = unprocessed
|
||||
break
|
||||
|
||||
text0, text1, text2 = section
|
||||
|
||||
if processed:
|
||||
processed = '\n'.join((processed, text0,))
|
||||
else:
|
||||
processed = text0
|
||||
|
||||
# print 'before:'
|
||||
# temp_lines = text1.split('\n')
|
||||
# for index, line in enumerate(temp_lines):
|
||||
# if index < 3 or index + 3 >= len(temp_lines):
|
||||
# print '\t', index, line
|
||||
# # print text1
|
||||
# print
|
||||
text1 = sort_func(text1, filepath, filename, file_extension)
|
||||
# print 'after:'
|
||||
# # print text1
|
||||
# temp_lines = text1.split('\n')
|
||||
# for index, line in enumerate(temp_lines):
|
||||
# if index < 3 or index + 3 >= len(temp_lines):
|
||||
# print '\t', index, line
|
||||
# print
|
||||
processed = '\n'.join((processed, text1,))
|
||||
if text2:
|
||||
unprocessed = text2
|
||||
else:
|
||||
break
|
||||
|
||||
if text != processed:
|
||||
print sort_name, filepath
|
||||
return processed
|
||||
|
||||
|
||||
def find_class_statement_section(text):
|
||||
def is_class_statement(line):
|
||||
return line.strip().startswith('@class ')
|
||||
|
||||
return find_matching_section(text, is_class_statement)
|
||||
|
||||
|
||||
def find_include_section(text):
|
||||
def is_include_line(line):
|
||||
return is_include_or_import(line)
|
||||
# return is_include_or_import_or_empty(line)
|
||||
|
||||
return find_matching_section(text, is_include_line)
|
||||
|
||||
|
||||
def sort_includes(filepath, filename, file_extension, text):
|
||||
# print 'sort_includes', filepath
|
||||
if file_extension not in ('.h', '.m', '.mm'):
|
||||
return text
|
||||
return sort_matching_blocks('sort_includes', filepath, filename, file_extension, text, find_include_section, sort_include_block)
|
||||
|
||||
|
||||
def sort_class_statements(filepath, filename, file_extension, text):
|
||||
# print 'sort_class_statements', filepath
|
||||
if file_extension not in ('.h', '.m', '.mm'):
|
||||
return text
|
||||
return sort_matching_blocks('sort_class_statements', filepath, filename, file_extension, text, find_class_statement_section, sort_class_statement_block)
|
||||
|
||||
|
||||
def splitall(path):
|
||||
allparts = []
|
||||
while 1:
|
||||
parts = os.path.split(path)
|
||||
if parts[0] == path: # sentinel for absolute paths
|
||||
allparts.insert(0, parts[0])
|
||||
break
|
||||
elif parts[1] == path: # sentinel for relative paths
|
||||
allparts.insert(0, parts[1])
|
||||
break
|
||||
else:
|
||||
path = parts[0]
|
||||
allparts.insert(0, parts[1])
|
||||
return allparts
|
||||
|
||||
|
||||
def process(filepath):
|
||||
|
||||
short_filepath = filepath[len(git_repo_path):]
|
||||
if short_filepath.startswith(os.sep):
|
||||
short_filepath = short_filepath[len(os.sep):]
|
||||
|
||||
filename = os.path.basename(filepath)
|
||||
if filename.startswith('.'):
|
||||
raise "shouldn't call process with dotfile"
|
||||
file_ext = os.path.splitext(filename)[1]
|
||||
if file_ext in ('.swift'):
|
||||
env_copy = os.environ.copy()
|
||||
env_copy["SCRIPT_INPUT_FILE_COUNT"] = "1"
|
||||
env_copy["SCRIPT_INPUT_FILE_0"] = '%s' % ( short_filepath, )
|
||||
lint_output = subprocess.check_output(['swiftlint', 'autocorrect', '--use-script-input-files'], env=env_copy)
|
||||
print lint_output
|
||||
try:
|
||||
lint_output = subprocess.check_output(['swiftlint', 'lint', '--use-script-input-files'], env=env_copy)
|
||||
except subprocess.CalledProcessError, e:
|
||||
lint_output = e.output
|
||||
print lint_output
|
||||
|
||||
with open(filepath, 'rt') as f:
|
||||
text = f.read()
|
||||
|
||||
original_text = text
|
||||
|
||||
text = sort_includes(filepath, filename, file_ext, text)
|
||||
text = sort_class_statements(filepath, filename, file_ext, text)
|
||||
|
||||
lines = text.split('\n')
|
||||
while lines and lines[0].startswith('//'):
|
||||
lines = lines[1:]
|
||||
text = '\n'.join(lines)
|
||||
text = text.strip()
|
||||
|
||||
header = '''//
|
||||
// Copyright (c) %s Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
''' % (
|
||||
datetime.datetime.now().year,
|
||||
)
|
||||
text = header + text + '\n'
|
||||
|
||||
if original_text == text:
|
||||
return
|
||||
|
||||
print 'Updating:', short_filepath
|
||||
|
||||
with open(filepath, 'wt') as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
def should_ignore_path(path):
|
||||
ignore_paths = [
|
||||
os.path.join(git_repo_path, '.git')
|
||||
]
|
||||
for ignore_path in ignore_paths:
|
||||
if path.startswith(ignore_path):
|
||||
return True
|
||||
for component in splitall(path):
|
||||
if component.startswith('.'):
|
||||
return True
|
||||
if component.endswith('.framework'):
|
||||
return True
|
||||
if component in ('Pods', 'ThirdParty', 'Carthage',):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def process_if_appropriate(filepath):
|
||||
filename = os.path.basename(filepath)
|
||||
if filename.startswith('.'):
|
||||
return
|
||||
file_ext = os.path.splitext(filename)[1]
|
||||
if file_ext not in ('.h', '.hpp', '.cpp', '.m', '.mm', '.pch', '.swift'):
|
||||
return
|
||||
if should_ignore_path(filepath):
|
||||
return
|
||||
process(filepath)
|
||||
|
||||
|
||||
def check_diff_for_keywords():
|
||||
objc_keywords = [
|
||||
"OWSAbstractMethod\("
|
||||
"OWSAssert\(",
|
||||
"OWSCAssert\(",
|
||||
"OWSFail\(",
|
||||
"OWSCFail\(",
|
||||
"ows_add_overflow\(",
|
||||
"ows_sub_overflow\(",
|
||||
]
|
||||
|
||||
swift_keywords = [
|
||||
"owsFail\(",
|
||||
"precondition\(",
|
||||
"fatalError\(",
|
||||
"dispatchPrecondition\(",
|
||||
"preconditionFailure\(",
|
||||
"notImplemented\("
|
||||
]
|
||||
|
||||
keywords = objc_keywords + swift_keywords
|
||||
|
||||
matching_expression = "|".join(keywords)
|
||||
command_line = 'git diff --staged | grep --color=always -C 3 -E "%s"' % matching_expression
|
||||
try:
|
||||
output = subprocess.check_output(command_line, shell=True)
|
||||
except subprocess.CalledProcessError, e:
|
||||
# > man grep
|
||||
# EXIT STATUS
|
||||
# The grep utility exits with one of the following values:
|
||||
# 0 One or more lines were selected.
|
||||
# 1 No lines were selected.
|
||||
# >1 An error occurred.
|
||||
if e.returncode == 1:
|
||||
# no keywords in diff output
|
||||
return
|
||||
else:
|
||||
# some other error - bad grep expression?
|
||||
raise e
|
||||
|
||||
if len(output) > 0:
|
||||
print("⚠️ keywords detected in diff:")
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(description='Precommit script.')
|
||||
parser.add_argument('--all', action='store_true', help='process all files in or below current dir')
|
||||
parser.add_argument('--path', help='used to specify a path to process.')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.all:
|
||||
for rootdir, dirnames, filenames in os.walk(git_repo_path):
|
||||
for filename in filenames:
|
||||
file_path = os.path.abspath(os.path.join(rootdir, filename))
|
||||
process_if_appropriate(file_path)
|
||||
elif args.path:
|
||||
for rootdir, dirnames, filenames in os.walk(args.path):
|
||||
for filename in filenames:
|
||||
file_path = os.path.abspath(os.path.join(rootdir, filename))
|
||||
process_if_appropriate(file_path)
|
||||
else:
|
||||
filepaths = []
|
||||
|
||||
# Staging
|
||||
output = commands.getoutput('git diff --cached --name-only --diff-filter=ACMR')
|
||||
filepaths.extend([line.strip() for line in output.split('\n')])
|
||||
|
||||
# Working
|
||||
output = commands.getoutput('git diff --name-only --diff-filter=ACMR')
|
||||
filepaths.extend([line.strip() for line in output.split('\n')])
|
||||
|
||||
# Only process each path once.
|
||||
filepaths = sorted(set(filepaths))
|
||||
|
||||
for filepath in filepaths:
|
||||
filepath = os.path.abspath(os.path.join(git_repo_path, filepath))
|
||||
process_if_appropriate(filepath)
|
||||
|
||||
print 'git clang-format...'
|
||||
print commands.getoutput('git clang-format')
|
||||
|
||||
check_diff_for_keywords()
|
|
@ -1,98 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# When we make a hotfix, we need to reverse integrate our hotfix back into
|
||||
# master. After commiting to master, this script audits that all tags have been
|
||||
# reverse integrated.
|
||||
import subprocess
|
||||
from distutils.version import LooseVersion
|
||||
import logging
|
||||
|
||||
#logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
def is_on_master():
|
||||
output = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).strip()
|
||||
logging.debug("branch output: %s" % output)
|
||||
return output == "master"
|
||||
|
||||
def main():
|
||||
if not is_on_master():
|
||||
# Don't interfere while on a feature or hotfix branch
|
||||
logging.debug("not on master branch")
|
||||
return
|
||||
|
||||
logging.debug("on master branch")
|
||||
|
||||
unmerged_tags_output = subprocess.check_output(["git", "tag", "--no-merged", "master"])
|
||||
unmerged_tags = [line.strip() for line in unmerged_tags_output.split("\n") if len(line) > 0]
|
||||
|
||||
logging.debug("All unmerged tags: %s" % unmerged_tags)
|
||||
|
||||
# Before this point we weren't always reverse integrating our tags. As we
|
||||
# audit old tags, we can ratchet this version number back.
|
||||
epoch_tag="2.21.0"
|
||||
|
||||
logging.debug("ignoring tags before epoch_tag: %s" % epoch_tag)
|
||||
|
||||
tags_of_concern = [tag for tag in unmerged_tags if LooseVersion(tag) > LooseVersion(epoch_tag)]
|
||||
|
||||
# Don't reverse integrate tags for adhoc builds
|
||||
tags_of_concern = [tag for tag in tags_of_concern if "adhoc" not in tag]
|
||||
|
||||
tags_to_ignore = [
|
||||
'2.23.3.0',
|
||||
'2.23.3.1',
|
||||
'2.26.0.6',
|
||||
'2.26.0.7',
|
||||
'2.26.0.15',
|
||||
'2.26.0.16',
|
||||
'2.29.0.7',
|
||||
'2.29.0.8',
|
||||
'2.29.0.9',
|
||||
'2.29.0.11',
|
||||
'2.30.0.0',
|
||||
'2.30.0.1',
|
||||
'2.30.2.0',
|
||||
'3.0',
|
||||
'3.0.1',
|
||||
'3.0.2',
|
||||
# These tags were from unmerged branches investigating an issue that only reproduced when installed from TF.
|
||||
'2.34.0.10', '2.34.0.11', '2.34.0.12', '2.34.0.13', '2.34.0.15', '2.34.0.16', '2.34.0.17', '2.34.0.18', '2.34.0.19', '2.34.0.20', '2.34.0.6', '2.34.0.7', '2.34.0.8', '2.34.0.9',
|
||||
'2.37.3.0',
|
||||
'2.37.4.0',
|
||||
# these were internal release only tags, now we include "-internal" in the tag name to avoid this
|
||||
'2.38.0.2.1',
|
||||
'2.38.0.3.1',
|
||||
'2.38.0.4.1',
|
||||
# the work in these tags was moved to the 2.38.1 release instead
|
||||
'2.38.0.12',
|
||||
'2.38.0.13',
|
||||
'2.38.0.14',
|
||||
#
|
||||
]
|
||||
tags_of_concern = [tag for tag in tags_of_concern if tag not in tags_to_ignore]
|
||||
|
||||
# Interal Builds
|
||||
#
|
||||
# If you want to tag a build which is not intended to be reverse
|
||||
# integrated, include the text "internal" somewhere in the tag name, such as
|
||||
#
|
||||
# 1.2.3.4.5-internal
|
||||
# 1.2.3.4.5-internal-mkirk
|
||||
#
|
||||
# NOTE: that if you upload the build to test flight, you still need to give testflight
|
||||
# a numeric build number - so tag won't match the build number exactly as they do
|
||||
# with production build tags. That's fine.
|
||||
#
|
||||
# To avoid collision with "production" build numbers, use at least a 5
|
||||
# digit build number.
|
||||
tags_of_concern = [tag for tag in tags_of_concern if "internal" not in tag]
|
||||
|
||||
if len(tags_of_concern) > 0:
|
||||
logging.debug("Found unmerged tags newer than epoch: %s" % tags_of_concern)
|
||||
raise RuntimeError("💥 Found unmerged tags: %s" % tags_of_concern)
|
||||
else:
|
||||
logging.debug("No unmerged tags newer than epoch. All good!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,37 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# PROJECT_DIR will be set when run from xcode, else we infer it
|
||||
if [ "${PROJECT_DIR}" = "" ]; then
|
||||
PROJECT_DIR=`git rev-parse --show-toplevel`
|
||||
echo "inferred ${PROJECT_DIR}"
|
||||
fi
|
||||
|
||||
# Capture hash & comment from last WebRTC git commit.
|
||||
cd $PROJECT_DIR/ThirdParty/WebRTC/
|
||||
_git_commit=`git log --pretty=oneline | head -1`
|
||||
cd $PROJECT_DIR
|
||||
|
||||
# Remove existing .plist entry, if any.
|
||||
/usr/libexec/PlistBuddy -c "Delete BuildDetails" Signal/Signal-Info.plist || true
|
||||
# Add new .plist entry.
|
||||
/usr/libexec/PlistBuddy -c "add BuildDetails dict" Signal/Signal-Info.plist
|
||||
|
||||
/usr/libexec/PlistBuddy -c "add :BuildDetails:WebRTCCommit string '$_git_commit'" Signal/Signal-Info.plist
|
||||
|
||||
_osx_version=`defaults read loginwindow SystemVersionStampAsString`
|
||||
/usr/libexec/PlistBuddy -c "add :BuildDetails:OSXVersion string '$_osx_version'" Signal/Signal-Info.plist
|
||||
|
||||
_carthage_version=`carthage version`
|
||||
/usr/libexec/PlistBuddy -c "add :BuildDetails:CarthageVersion string '$_carthage_version'" Signal/Signal-Info.plist
|
||||
|
||||
echo "CONFIGURATION: ${CONFIGURATION}"
|
||||
if [ "${CONFIGURATION}" = "App Store Release" ]; then
|
||||
/usr/libexec/PlistBuddy -c "add :BuildDetails:XCodeVersion string '${XCODE_VERSION_MAJOR}.${XCODE_VERSION_MINOR}'" Signal/Signal-Info.plist
|
||||
|
||||
# Use UTC
|
||||
_build_datetime=`date -u`
|
||||
/usr/libexec/PlistBuddy -c "add :BuildDetails:DateTime string '$_build_datetime'" Signal/Signal-Info.plist
|
||||
fi
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
|
||||
extension AppDelegate : OpenGroupAPIDelegate {
|
||||
|
||||
public func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: OpenGroupInfo) {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
let publicChatID = "\(server).\(channel)"
|
||||
Storage.writeSync { transaction in
|
||||
// Update user count
|
||||
storage.setUserCount(info.memberCount, forPublicChatWithID: publicChatID, in: transaction)
|
||||
let groupThread = TSGroupThread.getOrCreateThread(withGroupId: publicChatID.data(using: .utf8)!, groupType: .openGroup, transaction: transaction)
|
||||
// Update display name if needed
|
||||
let groupModel = groupThread.groupModel
|
||||
if groupModel.groupName != info.displayName {
|
||||
let newGroupModel = TSGroupModel(title: info.displayName, memberIds: groupModel.groupMemberIds, image: groupModel.groupImage, groupId: groupModel.groupId, groupType: groupModel.groupType, adminIds: groupModel.groupAdminIds)
|
||||
groupThread.groupModel = newGroupModel
|
||||
groupThread.save(with: transaction)
|
||||
}
|
||||
// Download and update profile picture if needed
|
||||
let oldProfilePictureURL = storage.getProfilePictureURL(forPublicChatWithID: publicChatID, in: transaction)
|
||||
if oldProfilePictureURL != info.profilePictureURL || groupModel.groupImage == nil {
|
||||
storage.setProfilePictureURL(info.profilePictureURL, forPublicChatWithID: publicChatID, in: transaction)
|
||||
if let profilePictureURL = info.profilePictureURL {
|
||||
var sanitizedServerURL = server
|
||||
var sanitizedProfilePictureURL = profilePictureURL
|
||||
while sanitizedServerURL.hasSuffix("/") { sanitizedServerURL.removeLast(1) }
|
||||
while sanitizedProfilePictureURL.hasPrefix("/") { sanitizedProfilePictureURL.removeFirst(1) }
|
||||
let url = "\(sanitizedServerURL)/\(sanitizedProfilePictureURL)"
|
||||
FileServerAPI.downloadAttachment(from: url).map2 { data in
|
||||
let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(data.count), sourceFilename: nil, caption: nil, albumMessageId: nil)
|
||||
try attachmentStream.write(data)
|
||||
groupThread.updateAvatar(with: attachmentStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
extension AppDelegate : SharedSenderKeysDelegate {
|
||||
|
||||
public func requestSenderKey(for groupPublicKey: String, senderPublicKey: String, using transaction: Any) {
|
||||
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction as! YapDatabaseReadWriteTransaction)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
import UIKit
|
||||
|
||||
final class ConversationCell : UITableViewCell {
|
||||
var threadViewModel: ThreadViewModel! { didSet { update() } }
|
||||
|
||||
static let reuseIdentifier = "ConversationCell"
|
||||
|
||||
// MARK: Components
|
||||
private let accentView = UIView()
|
||||
|
||||
private lazy var profilePictureView = ProfilePictureView()
|
||||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var timestampLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
result.alpha = Values.conversationCellTimestampOpacity
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var snippetLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var typingIndicatorView = TypingIndicatorView()
|
||||
|
||||
private lazy var statusIndicatorView: UIImageView = {
|
||||
let result = UIImageView()
|
||||
result.contentMode = .scaleAspectFit
|
||||
result.layer.cornerRadius = Values.conversationCellStatusIndicatorSize / 2
|
||||
result.layer.masksToBounds = true
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Initialization
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
let cellHeight: CGFloat = 68
|
||||
// Set the cell background color
|
||||
backgroundColor = Colors.cellBackground
|
||||
// Set up the highlight color
|
||||
let selectedBackgroundView = UIView()
|
||||
selectedBackgroundView.backgroundColor = Colors.cellSelected
|
||||
self.selectedBackgroundView = selectedBackgroundView
|
||||
// Set up the accent view
|
||||
accentView.set(.width, to: Values.accentLineThickness)
|
||||
accentView.set(.height, to: cellHeight)
|
||||
// Set up the profile picture view
|
||||
let profilePictureViewSize = Values.mediumProfilePictureSize
|
||||
profilePictureView.set(.width, to: profilePictureViewSize)
|
||||
profilePictureView.set(.height, to: profilePictureViewSize)
|
||||
profilePictureView.size = profilePictureViewSize
|
||||
// Set up the label stack view
|
||||
let topLabelSpacer = UIView.hStretchingSpacer()
|
||||
let topLabelStackView = UIStackView(arrangedSubviews: [ displayNameLabel, topLabelSpacer, timestampLabel ])
|
||||
topLabelStackView.axis = .horizontal
|
||||
topLabelStackView.alignment = .center
|
||||
topLabelStackView.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
||||
let snippetLabelContainer = UIView()
|
||||
snippetLabelContainer.addSubview(snippetLabel)
|
||||
snippetLabelContainer.addSubview(typingIndicatorView)
|
||||
let bottomLabelSpacer = UIView.hStretchingSpacer()
|
||||
let bottomLabelStackView = UIStackView(arrangedSubviews: [ snippetLabelContainer, bottomLabelSpacer, statusIndicatorView ])
|
||||
bottomLabelStackView.axis = .horizontal
|
||||
bottomLabelStackView.alignment = .center
|
||||
bottomLabelStackView.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
||||
let labelContainerView = UIView()
|
||||
labelContainerView.addSubview(topLabelStackView)
|
||||
labelContainerView.addSubview(bottomLabelStackView)
|
||||
// Set up the main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ accentView, profilePictureView, labelContainerView ])
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
contentView.addSubview(stackView)
|
||||
// Set up the constraints
|
||||
accentView.pin(.top, to: .top, of: contentView)
|
||||
accentView.pin(.bottom, to: .bottom, of: contentView)
|
||||
// The three lines below are part of a workaround for a weird layout bug
|
||||
topLabelStackView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - Values.mediumSpacing - profilePictureViewSize - Values.mediumSpacing - Values.mediumSpacing)
|
||||
topLabelStackView.set(.height, to: 20)
|
||||
topLabelSpacer.set(.height, to: 20)
|
||||
timestampLabel.setContentCompressionResistancePriority(.required, for: NSLayoutConstraint.Axis.horizontal)
|
||||
// The three lines below are part of a workaround for a weird layout bug
|
||||
bottomLabelStackView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - Values.mediumSpacing - profilePictureViewSize - Values.mediumSpacing - Values.mediumSpacing)
|
||||
bottomLabelStackView.set(.height, to: 18)
|
||||
bottomLabelSpacer.set(.height, to: 18)
|
||||
statusIndicatorView.set(.width, to: Values.conversationCellStatusIndicatorSize)
|
||||
statusIndicatorView.set(.height, to: Values.conversationCellStatusIndicatorSize)
|
||||
snippetLabel.pin(to: snippetLabelContainer)
|
||||
typingIndicatorView.pin(.leading, to: .leading, of: snippetLabelContainer)
|
||||
typingIndicatorView.centerYAnchor.constraint(equalTo: snippetLabel.centerYAnchor).isActive = true
|
||||
// Not using a stack view for this is part of a workaround for a weird layout bug
|
||||
topLabelStackView.pin(.leading, to: .leading, of: labelContainerView)
|
||||
topLabelStackView.pin(.top, to: .top, of: labelContainerView, withInset: 12)
|
||||
topLabelStackView.pin(.trailing, to: .trailing, of: labelContainerView)
|
||||
bottomLabelStackView.pin(.leading, to: .leading, of: labelContainerView)
|
||||
bottomLabelStackView.pin(.top, to: .bottom, of: topLabelStackView, withInset: 6)
|
||||
labelContainerView.pin(.bottom, to: .bottom, of: bottomLabelStackView, withInset: 12)
|
||||
// The two lines below are part of a workaround for a weird layout bug
|
||||
labelContainerView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - Values.mediumSpacing - profilePictureViewSize - Values.mediumSpacing - Values.mediumSpacing)
|
||||
labelContainerView.set(.height, to: cellHeight)
|
||||
stackView.pin(.leading, to: .leading, of: contentView)
|
||||
stackView.pin(.top, to: .top, of: contentView)
|
||||
// The two lines below are part of a workaround for a weird layout bug
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width - Values.mediumSpacing)
|
||||
stackView.set(.height, to: cellHeight)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func update() {
|
||||
AssertIsOnMainThread()
|
||||
let thread = threadViewModel.threadRecord
|
||||
guard let threadID = thread.uniqueId else { return }
|
||||
MentionsManager.populateUserPublicKeyCacheIfNeeded(for: threadID) // FIXME: This is a terrible place to do this
|
||||
let isBlocked: Bool
|
||||
if let thread = thread as? TSContactThread {
|
||||
isBlocked = SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(thread.contactIdentifier())
|
||||
} else {
|
||||
isBlocked = false
|
||||
}
|
||||
if isBlocked {
|
||||
accentView.backgroundColor = Colors.destructive
|
||||
accentView.alpha = 1
|
||||
} else {
|
||||
accentView.backgroundColor = Colors.accent
|
||||
accentView.alpha = threadViewModel.hasUnreadMessages ? 1 : 0.0001 // Setting the alpha to exactly 0 causes an issue on iOS 12
|
||||
}
|
||||
profilePictureView.update(for: thread)
|
||||
displayNameLabel.text = getDisplayName()
|
||||
timestampLabel.text = DateUtil.formatDateShort(threadViewModel.lastMessageDate)
|
||||
if SSKEnvironment.shared.typingIndicators.typingRecipientId(forThread: thread) != nil {
|
||||
snippetLabel.text = ""
|
||||
typingIndicatorView.isHidden = false
|
||||
typingIndicatorView.startAnimation()
|
||||
} else {
|
||||
snippetLabel.attributedText = getSnippet()
|
||||
typingIndicatorView.isHidden = true
|
||||
typingIndicatorView.stopAnimation()
|
||||
}
|
||||
statusIndicatorView.backgroundColor = nil
|
||||
let lastMessage = threadViewModel.lastMessageForInbox
|
||||
if let lastMessage = lastMessage as? TSOutgoingMessage {
|
||||
let image: UIImage
|
||||
let status = MessageRecipientStatusUtils.recipientStatus(outgoingMessage: lastMessage)
|
||||
switch status {
|
||||
case .calculatingPoW, .uploading, .sending: image = #imageLiteral(resourceName: "CircleDotDotDot").asTintedImage(color: Colors.text)!
|
||||
case .sent, .skipped, .delivered: image = #imageLiteral(resourceName: "CircleCheck").asTintedImage(color: Colors.text)!
|
||||
case .read:
|
||||
statusIndicatorView.backgroundColor = isLightMode ? .black : .white
|
||||
image = isLightMode ? #imageLiteral(resourceName: "FilledCircleCheckLightMode") : #imageLiteral(resourceName: "FilledCircleCheckDarkMode")
|
||||
case .failed: image = #imageLiteral(resourceName: "message_status_failed").asTintedImage(color: Colors.text)!
|
||||
}
|
||||
statusIndicatorView.image = image
|
||||
statusIndicatorView.isHidden = false
|
||||
} else {
|
||||
statusIndicatorView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
private func getDisplayName() -> String {
|
||||
if threadViewModel.isGroupThread {
|
||||
if threadViewModel.name.isEmpty {
|
||||
return GroupDisplayNameUtilities.getDefaultDisplayName(for: threadViewModel.threadRecord as! TSGroupThread)
|
||||
} else {
|
||||
return threadViewModel.name
|
||||
}
|
||||
} else {
|
||||
if threadViewModel.threadRecord.isNoteToSelf() {
|
||||
return NSLocalizedString("NOTE_TO_SELF", comment: "")
|
||||
} else {
|
||||
let hexEncodedPublicKey = threadViewModel.contactIdentifier!
|
||||
return UserDisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) ?? hexEncodedPublicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getSnippet() -> NSMutableAttributedString {
|
||||
let result = NSMutableAttributedString()
|
||||
if threadViewModel.isMuted {
|
||||
result.append(NSAttributedString(string: "\u{e067} ", attributes: [ .font : UIFont.ows_elegantIconsFont(10), .foregroundColor : Colors.unimportant ]))
|
||||
}
|
||||
if let rawSnippet = threadViewModel.lastMessageText {
|
||||
let snippet = MentionUtilities.highlightMentions(in: rawSnippet, threadID: threadViewModel.threadRecord.uniqueId!)
|
||||
let font = threadViewModel.hasUnreadMessages ? UIFont.boldSystemFont(ofSize: Values.smallFontSize) : UIFont.systemFont(ofSize: Values.smallFontSize)
|
||||
result.append(NSAttributedString(string: snippet, attributes: [ .font : font, .foregroundColor : Colors.text ]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
|
||||
// MARK: - User Selection View
|
||||
|
||||
@objc(LKMentionCandidateSelectionView)
|
||||
final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITableViewDelegate {
|
||||
@objc var mentionCandidates: [Mention] = [] { didSet { tableView.reloadData() } }
|
||||
@objc var publicChatServer: String?
|
||||
var publicChatChannel: UInt64?
|
||||
@objc var delegate: MentionCandidateSelectionViewDelegate?
|
||||
|
||||
// MARK: Convenience
|
||||
@objc(setPublicChatChannel:)
|
||||
func setPublicChatChannel(to publicChatChannel: UInt64) {
|
||||
self.publicChatChannel = publicChatChannel != 0 ? publicChatChannel : nil
|
||||
}
|
||||
|
||||
// MARK: Components
|
||||
@objc lazy var tableView: UITableView = { // TODO: Make this private
|
||||
let result = UITableView()
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
result.register(Cell.self, forCellReuseIdentifier: "Cell")
|
||||
result.separatorStyle = .none
|
||||
result.backgroundColor = .clear
|
||||
result.showsVerticalScrollIndicator = false
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Initialization
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
addSubview(tableView)
|
||||
tableView.pin(to: self)
|
||||
let topSeparator = UIView()
|
||||
topSeparator.backgroundColor = Colors.separator
|
||||
topSeparator.set(.height, to: Values.separatorThickness)
|
||||
addSubview(topSeparator)
|
||||
topSeparator.pin(.leading, to: .leading, of: self)
|
||||
topSeparator.pin(.top, to: .top, of: self)
|
||||
topSeparator.pin(.trailing, to: .trailing, of: self)
|
||||
let bottomSeparator = UIView()
|
||||
bottomSeparator.backgroundColor = Colors.separator
|
||||
bottomSeparator.set(.height, to: Values.separatorThickness)
|
||||
addSubview(bottomSeparator)
|
||||
bottomSeparator.pin(.leading, to: .leading, of: self)
|
||||
bottomSeparator.pin(.trailing, to: .trailing, of: self)
|
||||
bottomSeparator.pin(.bottom, to: .bottom, of: self)
|
||||
}
|
||||
|
||||
// MARK: Data
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return mentionCandidates.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
|
||||
let mentionCandidate = mentionCandidates[indexPath.row]
|
||||
cell.mentionCandidate = mentionCandidate
|
||||
cell.publicChatServer = publicChatServer
|
||||
cell.publicChatChannel = publicChatChannel
|
||||
cell.separator.isHidden = (indexPath.row == (mentionCandidates.count - 1))
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let mentionCandidate = mentionCandidates[indexPath.row]
|
||||
delegate?.handleMentionCandidateSelected(mentionCandidate, from: self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cell
|
||||
|
||||
private extension MentionCandidateSelectionView {
|
||||
|
||||
final class Cell : UITableViewCell {
|
||||
var mentionCandidate = Mention(publicKey: "", displayName: "") { didSet { update() } }
|
||||
var publicChatServer: String?
|
||||
var publicChatChannel: UInt64?
|
||||
|
||||
// MARK: Components
|
||||
private lazy var profilePictureView = ProfilePictureView()
|
||||
|
||||
private lazy var moderatorIconImageView: UIImageView = {
|
||||
let result = UIImageView(image: #imageLiteral(resourceName: "Crown"))
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
lazy var separator: UIView = {
|
||||
let result = UIView()
|
||||
result.backgroundColor = Colors.separator
|
||||
result.set(.height, to: Values.separatorThickness)
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Initialization
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
// Set the cell background color
|
||||
backgroundColor = Colors.cellBackground
|
||||
// Set up the highlight color
|
||||
let selectedBackgroundView = UIView()
|
||||
selectedBackgroundView.backgroundColor = Colors.cellBackground // Intentionally not Colors.cellSelected
|
||||
self.selectedBackgroundView = selectedBackgroundView
|
||||
// Set up the profile picture image view
|
||||
let profilePictureViewSize = Values.verySmallProfilePictureSize
|
||||
profilePictureView.set(.width, to: profilePictureViewSize)
|
||||
profilePictureView.set(.height, to: profilePictureViewSize)
|
||||
profilePictureView.size = profilePictureViewSize
|
||||
// Set up the main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel ])
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
stackView.set(.height, to: profilePictureViewSize)
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: Values.smallSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.mediumSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.smallSpacing)
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing)
|
||||
// Set up the moderator icon image view
|
||||
moderatorIconImageView.set(.width, to: 20)
|
||||
moderatorIconImageView.set(.height, to: 20)
|
||||
contentView.addSubview(moderatorIconImageView)
|
||||
moderatorIconImageView.pin(.trailing, to: .trailing, of: profilePictureView)
|
||||
moderatorIconImageView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: 3.5)
|
||||
// Set up the separator
|
||||
addSubview(separator)
|
||||
separator.pin(.leading, to: .leading, of: self)
|
||||
separator.pin(.trailing, to: .trailing, of: self)
|
||||
separator.pin(.bottom, to: .bottom, of: self)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func update() {
|
||||
displayNameLabel.text = mentionCandidate.displayName
|
||||
profilePictureView.hexEncodedPublicKey = mentionCandidate.publicKey
|
||||
profilePictureView.update()
|
||||
if let server = publicChatServer, let channel = publicChatChannel {
|
||||
let isUserModerator = OpenGroupAPI.isUserModerator(mentionCandidate.publicKey, for: channel, on: server)
|
||||
moderatorIconImageView.isHidden = !isUserModerator
|
||||
} else {
|
||||
moderatorIconImageView.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Delegate
|
||||
@objc(LKMentionCandidateSelectionViewDelegate)
|
||||
protocol MentionCandidateSelectionViewDelegate {
|
||||
|
||||
func handleMentionCandidateSelected(_ mentionCandidate: Mention, from mentionCandidateSelectionView: MentionCandidateSelectionView)
|
||||
}
|
|
@ -0,0 +1,299 @@
|
|||
import UIKit
|
||||
|
||||
final class NewConversationButtonSet : UIView {
|
||||
private var isUserDragging = false
|
||||
private var horizontalButtonConstraints: [NewConversationButton:NSLayoutConstraint] = [:]
|
||||
private var verticalButtonConstraints: [NewConversationButton:NSLayoutConstraint] = [:]
|
||||
private var expandedButton: NewConversationButton?
|
||||
var delegate: NewConversationButtonSetDelegate?
|
||||
|
||||
// MARK: Settings
|
||||
private let spacing = Values.largeSpacing
|
||||
private let iconSize = CGFloat(24)
|
||||
private let maxDragDistance = CGFloat(56)
|
||||
private let dragMargin = CGFloat(16)
|
||||
|
||||
// MARK: Components
|
||||
private lazy var mainButton = NewConversationButton(isMainButton: true, icon: #imageLiteral(resourceName: "Plus").scaled(to: CGSize(width: iconSize, height: iconSize)))
|
||||
private lazy var createNewPrivateChatButton = NewConversationButton(isMainButton: false, icon: #imageLiteral(resourceName: "Message").scaled(to: CGSize(width: iconSize, height: iconSize)))
|
||||
private lazy var createNewClosedGroupButton = NewConversationButton(isMainButton: false, icon: #imageLiteral(resourceName: "Group").scaled(to: CGSize(width: iconSize, height: iconSize)))
|
||||
private lazy var joinOpenGroupButton = NewConversationButton(isMainButton: false, icon: #imageLiteral(resourceName: "Globe").scaled(to: CGSize(width: iconSize, height: iconSize)))
|
||||
|
||||
// MARK: Initialization
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
let inset = (Values.newConversationButtonExpandedSize - Values.newConversationButtonCollapsedSize) / 2
|
||||
addSubview(joinOpenGroupButton)
|
||||
horizontalButtonConstraints[joinOpenGroupButton] = joinOpenGroupButton.pin(.left, to: .left, of: self, withInset: inset)
|
||||
verticalButtonConstraints[joinOpenGroupButton] = joinOpenGroupButton.pin(.bottom, to: .bottom, of: self, withInset: -inset)
|
||||
addSubview(createNewPrivateChatButton)
|
||||
createNewPrivateChatButton.center(.horizontal, in: self)
|
||||
verticalButtonConstraints[createNewPrivateChatButton] = createNewPrivateChatButton.pin(.top, to: .top, of: self, withInset: inset)
|
||||
addSubview(createNewClosedGroupButton)
|
||||
horizontalButtonConstraints[createNewClosedGroupButton] = createNewClosedGroupButton.pin(.right, to: .right, of: self, withInset: -inset)
|
||||
verticalButtonConstraints[createNewClosedGroupButton] = createNewClosedGroupButton.pin(.bottom, to: .bottom, of: self, withInset: -inset)
|
||||
addSubview(mainButton)
|
||||
mainButton.center(.horizontal, in: self)
|
||||
mainButton.pin(.bottom, to: .bottom, of: self, withInset: -inset)
|
||||
let width = 2 * Values.newConversationButtonExpandedSize + 2 * spacing + Values.newConversationButtonCollapsedSize
|
||||
set(.width, to: width)
|
||||
let height = Values.newConversationButtonExpandedSize + spacing + Values.newConversationButtonCollapsedSize
|
||||
set(.height, to: height)
|
||||
collapse(withAnimation: false)
|
||||
isUserInteractionEnabled = true
|
||||
let joinOpenGroupButtonTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleJoinOpenGroupButtonTapped))
|
||||
joinOpenGroupButton.addGestureRecognizer(joinOpenGroupButtonTapGestureRecognizer)
|
||||
let createNewPrivateChatButtonTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleCreateNewPrivateChatButtonTapped))
|
||||
createNewPrivateChatButton.addGestureRecognizer(createNewPrivateChatButtonTapGestureRecognizer)
|
||||
let createNewClosedGroupButtonTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleCreateNewClosedGroupButtonTapped))
|
||||
createNewClosedGroupButton.addGestureRecognizer(createNewClosedGroupButtonTapGestureRecognizer)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func handleJoinOpenGroupButtonTapped() { delegate?.joinOpenGroup() }
|
||||
@objc private func handleCreateNewPrivateChatButtonTapped() { delegate?.createNewPrivateChat() }
|
||||
@objc private func handleCreateNewClosedGroupButtonTapped() { delegate?.createNewClosedGroup() }
|
||||
|
||||
private func expand(isUserDragging: Bool) {
|
||||
let buttons = [ joinOpenGroupButton, createNewPrivateChatButton, createNewClosedGroupButton ]
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
buttons.forEach { $0.alpha = 1 }
|
||||
let inset = (Values.newConversationButtonExpandedSize - Values.newConversationButtonCollapsedSize) / 2
|
||||
let size = Values.newConversationButtonCollapsedSize
|
||||
self.joinOpenGroupButton.frame = CGRect(origin: CGPoint(x: inset, y: self.height() - size - inset), size: CGSize(width: size, height: size))
|
||||
self.createNewPrivateChatButton.frame = CGRect(center: CGPoint(x: self.bounds.center.x, y: inset + size / 2), size: CGSize(width: size, height: size))
|
||||
self.createNewClosedGroupButton.frame = CGRect(origin: CGPoint(x: self.width() - size - inset, y: self.height() - size - inset), size: CGSize(width: size, height: size))
|
||||
}, completion: { _ in
|
||||
self.isUserDragging = isUserDragging
|
||||
})
|
||||
}
|
||||
|
||||
private func collapse(withAnimation isAnimated: Bool) {
|
||||
isUserDragging = false
|
||||
let buttons = [ joinOpenGroupButton, createNewPrivateChatButton, createNewClosedGroupButton ]
|
||||
UIView.animate(withDuration: isAnimated ? 0.25 : 0) {
|
||||
buttons.forEach { button in
|
||||
button.alpha = 0
|
||||
let size = Values.newConversationButtonCollapsedSize
|
||||
button.frame = CGRect(center: self.mainButton.center, size: CGSize(width: size, height: size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reset() {
|
||||
let mainButtonLocationInSelfCoordinates = CGPoint(x: width() / 2, y: height() - Values.newConversationButtonExpandedSize / 2)
|
||||
let mainButtonSize = mainButton.frame.size
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.mainButton.frame = CGRect(center: mainButtonLocationInSelfCoordinates, size: mainButtonSize)
|
||||
self.mainButton.alpha = 1
|
||||
}
|
||||
if let expandedButton = expandedButton { collapse(expandedButton) }
|
||||
expandedButton = nil
|
||||
Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in
|
||||
self.collapse(withAnimation: true)
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard let touch = touches.first, mainButton.contains(touch), !isUserDragging else { return }
|
||||
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||
expand(isUserDragging: true)
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard let touch = touches.first, isUserDragging else { return }
|
||||
let mainButtonSize = mainButton.frame.size
|
||||
let mainButtonLocationInSelfCoordinates = CGPoint(x: width() / 2, y: height() - Values.newConversationButtonExpandedSize / 2)
|
||||
let touchLocationInSelfCoordinates = touch.location(in: self)
|
||||
mainButton.frame = CGRect(center: touchLocationInSelfCoordinates, size: mainButtonSize)
|
||||
mainButton.alpha = 1 - (touchLocationInSelfCoordinates.distance(to: mainButtonLocationInSelfCoordinates) / maxDragDistance)
|
||||
let buttons = [ joinOpenGroupButton, createNewPrivateChatButton, createNewClosedGroupButton ]
|
||||
let buttonToExpand = buttons.first { button in
|
||||
var hasUserDraggedBeyondButton = false
|
||||
if button == joinOpenGroupButton && touch.isLeft(of: joinOpenGroupButton, with: dragMargin) { hasUserDraggedBeyondButton = true }
|
||||
if button == createNewPrivateChatButton && touch.isAbove(createNewPrivateChatButton, with: dragMargin) { hasUserDraggedBeyondButton = true }
|
||||
if button == createNewClosedGroupButton && touch.isRight(of: createNewClosedGroupButton, with: dragMargin) { hasUserDraggedBeyondButton = true }
|
||||
return button.contains(touch) || hasUserDraggedBeyondButton
|
||||
}
|
||||
if let buttonToExpand = buttonToExpand {
|
||||
guard buttonToExpand != expandedButton else { return }
|
||||
if let expandedButton = expandedButton { collapse(expandedButton) }
|
||||
expand(buttonToExpand)
|
||||
expandedButton = buttonToExpand
|
||||
} else {
|
||||
if let expandedButton = expandedButton { collapse(expandedButton) }
|
||||
expandedButton = nil
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard let touch = touches.first, isUserDragging else { return }
|
||||
if joinOpenGroupButton.contains(touch) || touch.isLeft(of: joinOpenGroupButton, with: dragMargin) { delegate?.joinOpenGroup() }
|
||||
else if createNewPrivateChatButton.contains(touch) || touch.isAbove(createNewPrivateChatButton, with: dragMargin) { delegate?.createNewPrivateChat() }
|
||||
else if createNewClosedGroupButton.contains(touch) || touch.isRight(of: createNewClosedGroupButton, with: dragMargin) { delegate?.createNewClosedGroup() }
|
||||
reset()
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard isUserDragging else { return }
|
||||
reset()
|
||||
}
|
||||
|
||||
private func expand(_ button: NewConversationButton) {
|
||||
if let horizontalConstraint = horizontalButtonConstraints[button] { horizontalConstraint.constant = 0 }
|
||||
if let verticalConstraint = verticalButtonConstraints[button] { verticalConstraint.constant = 0 }
|
||||
let size = Values.newConversationButtonExpandedSize
|
||||
let frame = CGRect(center: button.center, size: CGSize(width: size, height: size))
|
||||
button.widthConstraint.constant = size
|
||||
button.heightConstraint.constant = size
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.layoutIfNeeded()
|
||||
button.frame = frame
|
||||
button.layer.cornerRadius = size / 2
|
||||
let glowColor = Colors.newConversationButtonShadow
|
||||
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: true, radius: isLightMode ? 4 : 6)
|
||||
button.setCircularGlow(with: glowConfiguration)
|
||||
button.backgroundColor = Colors.accent
|
||||
}
|
||||
}
|
||||
|
||||
private func collapse(_ button: NewConversationButton) {
|
||||
let inset = (Values.newConversationButtonExpandedSize - Values.newConversationButtonCollapsedSize) / 2
|
||||
if joinOpenGroupButton == expandedButton {
|
||||
horizontalButtonConstraints[joinOpenGroupButton]!.constant = inset
|
||||
verticalButtonConstraints[joinOpenGroupButton]!.constant = -inset
|
||||
} else if createNewPrivateChatButton == expandedButton {
|
||||
verticalButtonConstraints[createNewPrivateChatButton]!.constant = inset
|
||||
} else if createNewClosedGroupButton == expandedButton {
|
||||
horizontalButtonConstraints[createNewClosedGroupButton]!.constant = -inset
|
||||
verticalButtonConstraints[createNewClosedGroupButton]!.constant = -inset
|
||||
}
|
||||
let size = Values.newConversationButtonCollapsedSize
|
||||
let frame = CGRect(center: button.center, size: CGSize(width: size, height: size))
|
||||
button.widthConstraint.constant = size
|
||||
button.heightConstraint.constant = size
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.layoutIfNeeded()
|
||||
button.frame = frame
|
||||
button.layer.cornerRadius = size / 2
|
||||
let glowColor = isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black
|
||||
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: true, radius: isLightMode ? 4 : 6)
|
||||
button.setCircularGlow(with: glowConfiguration)
|
||||
button.backgroundColor = Colors.newConversationButtonCollapsedBackground
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !bounds.contains(point), isUserDragging { collapse(withAnimation: true) }
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Delegate
|
||||
protocol NewConversationButtonSetDelegate {
|
||||
|
||||
func joinOpenGroup()
|
||||
func createNewPrivateChat()
|
||||
func createNewClosedGroup()
|
||||
}
|
||||
|
||||
// MARK: Button
|
||||
private final class NewConversationButton : UIImageView {
|
||||
private let isMainButton: Bool
|
||||
private let icon: UIImage
|
||||
var widthConstraint: NSLayoutConstraint!
|
||||
var heightConstraint: NSLayoutConstraint!
|
||||
|
||||
init(isMainButton: Bool, icon: UIImage) {
|
||||
self.isMainButton = isMainButton
|
||||
self.icon = icon
|
||||
super.init(frame: CGRect.zero)
|
||||
setUpViewHierarchy()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleAppModeChangedNotification(_:)), name: .appModeChanged, object: nil)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(isMainButton:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(isMainButton:) instead.")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy(isUpdate: Bool = false) {
|
||||
let newConversationButtonCollapsedBackground = isLightMode ? UIColor(hex: 0xF5F5F5) : UIColor(hex: 0x1F1F1F)
|
||||
backgroundColor = isMainButton ? Colors.accent : newConversationButtonCollapsedBackground
|
||||
let size = Values.newConversationButtonCollapsedSize
|
||||
layer.cornerRadius = size / 2
|
||||
let glowColor = isMainButton ? Colors.newConversationButtonShadow : (isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black)
|
||||
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: false, radius: isLightMode ? 4 : 6)
|
||||
setCircularGlow(with: glowConfiguration)
|
||||
layer.masksToBounds = false
|
||||
let iconColor = (isMainButton && isLightMode) ? UIColor.white : (isLightMode ? UIColor.black : UIColor.white)
|
||||
image = icon.asTintedImage(color: iconColor)!
|
||||
contentMode = .center
|
||||
if !isUpdate {
|
||||
widthConstraint = set(.width, to: size)
|
||||
heightConstraint = set(.height, to: size)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleAppModeChangedNotification(_ notification: Notification) {
|
||||
setUpViewHierarchy(isUpdate: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
private extension UIView {
|
||||
|
||||
func contains(_ touch: UITouch) -> Bool {
|
||||
return bounds.contains(touch.location(in: self))
|
||||
}
|
||||
}
|
||||
|
||||
private extension UITouch {
|
||||
|
||||
func isLeft(of view: UIView, with margin: CGFloat = 0) -> Bool {
|
||||
return isContainedVertically(in: view, with: margin) && location(in: view).x < view.bounds.minX
|
||||
}
|
||||
|
||||
func isAbove(_ view: UIView, with margin: CGFloat = 0) -> Bool {
|
||||
return isContainedHorizontally(in: view, with: margin) && location(in: view).y < view.bounds.minY
|
||||
}
|
||||
|
||||
func isRight(of view: UIView, with margin: CGFloat = 0) -> Bool {
|
||||
return isContainedVertically(in: view, with: margin) && location(in: view).x > view.bounds.maxX
|
||||
}
|
||||
|
||||
func isBelow(_ view: UIView, with margin: CGFloat = 0) -> Bool {
|
||||
return isContainedHorizontally(in: view, with: margin) && location(in: view).y > view.bounds.maxY
|
||||
}
|
||||
|
||||
private func isContainedHorizontally(in view: UIView, with margin: CGFloat = 0) -> Bool {
|
||||
return ((view.bounds.minX - margin)...(view.bounds.maxX + margin)) ~= location(in: view).x
|
||||
}
|
||||
|
||||
private func isContainedVertically(in view: UIView, with margin: CGFloat = 0) -> Bool {
|
||||
return ((view.bounds.minY - margin)...(view.bounds.maxY + margin)) ~= location(in: view).y
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGPoint {
|
||||
|
||||
func distance(to otherPoint: CGPoint) -> CGFloat {
|
||||
return sqrt(pow(self.x - otherPoint.x, 2) + pow(self.y - otherPoint.y, 2))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import UIKit
|
||||
|
||||
final class OptionView : UIView {
|
||||
private let title: String
|
||||
private let explanation: String
|
||||
private let delegate: OptionViewDelegate
|
||||
private let isRecommended: Bool
|
||||
var isSelected = false { didSet { handleIsSelectedChanged() } }
|
||||
|
||||
init(title: String, explanation: String, delegate: OptionViewDelegate, isRecommended: Bool = false) {
|
||||
self.title = title
|
||||
self.explanation = explanation
|
||||
self.delegate = delegate
|
||||
self.isRecommended = isRecommended
|
||||
super.init(frame: CGRect.zero)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(string:explanation:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(string:explanation:) instead.")
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
backgroundColor = Colors.pnOptionBackground
|
||||
// Round corners
|
||||
layer.cornerRadius = Values.pnOptionCornerRadius
|
||||
// Set up border
|
||||
layer.borderWidth = Values.borderThickness
|
||||
layer.borderColor = Colors.pnOptionBorder.cgColor
|
||||
// Set up shadow
|
||||
layer.shadowColor = UIColor.black.cgColor
|
||||
layer.shadowOffset = CGSize(width: 0, height: 0.8)
|
||||
layer.shadowOpacity = isLightMode ? 0.4 : 1
|
||||
layer.shadowRadius = isLightMode ? 4 : 6
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
titleLabel.text = title
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
|
||||
explanationLabel.text = explanation
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 4
|
||||
stackView.alignment = .fill
|
||||
addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: self, withInset: 12)
|
||||
stackView.pin(.top, to: .top, of: self, withInset: 12)
|
||||
self.pin(.trailing, to: .trailing, of: stackView, withInset: 12)
|
||||
self.pin(.bottom, to: .bottom, of: stackView, withInset: 12)
|
||||
// Set up recommended label if needed
|
||||
if isRecommended {
|
||||
let recommendedLabel = UILabel()
|
||||
recommendedLabel.textColor = Colors.accent
|
||||
recommendedLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||
recommendedLabel.text = NSLocalizedString("vc_pn_mode_recommended_option_tag", comment: "")
|
||||
stackView.addArrangedSubview(recommendedLabel)
|
||||
}
|
||||
// Set up tap gesture recognizer
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||
addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
|
||||
@objc private func handleTap() {
|
||||
isSelected = !isSelected
|
||||
}
|
||||
|
||||
private func handleIsSelectedChanged() {
|
||||
let animationDuration: TimeInterval = 0.25
|
||||
// Animate border color
|
||||
let newBorderColor = isSelected ? Colors.accent.cgColor : Colors.pnOptionBorder.cgColor
|
||||
let borderAnimation = CABasicAnimation(keyPath: "borderColor")
|
||||
borderAnimation.fromValue = layer.shadowColor
|
||||
borderAnimation.toValue = newBorderColor
|
||||
borderAnimation.duration = animationDuration
|
||||
layer.add(borderAnimation, forKey: borderAnimation.keyPath)
|
||||
layer.borderColor = newBorderColor
|
||||
// Animate shadow color
|
||||
let newShadowColor = isSelected ? Colors.newConversationButtonShadow.cgColor : UIColor.black.cgColor
|
||||
let shadowAnimation = CABasicAnimation(keyPath: "shadowColor")
|
||||
shadowAnimation.fromValue = layer.shadowColor
|
||||
shadowAnimation.toValue = newShadowColor
|
||||
shadowAnimation.duration = animationDuration
|
||||
layer.add(shadowAnimation, forKey: shadowAnimation.keyPath)
|
||||
layer.shadowColor = newShadowColor
|
||||
// Notify delegate
|
||||
if isSelected { delegate.optionViewDidActivate(self) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Option View Delegate
|
||||
protocol OptionViewDelegate {
|
||||
|
||||
func optionViewDidActivate(_ optionView: OptionView)
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import UIKit
|
||||
|
||||
final class PathStatusView : UIView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUpViewHierarchy()
|
||||
registerObservers()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
registerObservers()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
layer.cornerRadius = Values.pathStatusViewSize / 2
|
||||
layer.masksToBounds = false
|
||||
if OnionRequestAPI.paths.isEmpty {
|
||||
OnionRequestAPI.paths = Storage.getOnionRequestPaths()
|
||||
}
|
||||
let color = (!OnionRequestAPI.paths.isEmpty) ? Colors.accent : Colors.pathsBuilding
|
||||
setColor(to: color, isAnimated: false)
|
||||
}
|
||||
|
||||
private func registerObservers() {
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleBuildingPathsNotification), name: .buildingPaths, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handlePathsBuiltNotification), name: .pathsBuilt, object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private func setColor(to color: UIColor, isAnimated: Bool) {
|
||||
backgroundColor = color
|
||||
let size = Values.pathStatusViewSize
|
||||
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: color, isAnimated: isAnimated, radius: isLightMode ? 6 : 8)
|
||||
setCircularGlow(with: glowConfiguration)
|
||||
}
|
||||
|
||||
@objc private func handleBuildingPathsNotification() {
|
||||
setColor(to: Colors.pathsBuilding, isAnimated: true)
|
||||
}
|
||||
|
||||
@objc private func handlePathsBuiltNotification() {
|
||||
setColor(to: Colors.accent, isAnimated: true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import SessionUIKit
|
||||
|
||||
@objc(LKSessionRestorationView)
|
||||
final class SessionRestorationView : UIView {
|
||||
private let thread: TSThread
|
||||
@objc public var onRestore: (() -> Void)?
|
||||
@objc public var onDismiss: (() -> Void)?
|
||||
|
||||
// MARK: Lifecycle
|
||||
@objc init(thread: TSThread) {
|
||||
self.thread = thread;
|
||||
super.init(frame: CGRect.zero)
|
||||
initialize()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("Using SessionRestorationView.init(coder:) isn't allowed. Use SessionRestorationView.init(thread:) instead.") }
|
||||
override init(frame: CGRect) { fatalError("Using SessionRestorationView.init(frame:) isn't allowed. Use SessionRestorationView.init(thread:) instead.") }
|
||||
|
||||
private func initialize() {
|
||||
// Set up background
|
||||
backgroundColor = Colors.modalBackground
|
||||
layer.cornerRadius = Values.modalCornerRadius
|
||||
layer.masksToBounds = false
|
||||
layer.borderColor = Colors.modalBorder.cgColor
|
||||
layer.borderWidth = Values.borderThickness
|
||||
layer.shadowColor = UIColor.black.cgColor
|
||||
layer.shadowRadius = 8
|
||||
layer.shadowOpacity = 0.64
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
titleLabel.text = "Session Out of Sync"
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
titleLabel.textAlignment = .center
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = "Would you like to restore your session? This can help resolve issues. Your messages will be preserved."
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up restore button
|
||||
let restoreButton = UIButton()
|
||||
restoreButton.set(.height, to: Values.mediumButtonHeight)
|
||||
restoreButton.layer.cornerRadius = Values.modalButtonCornerRadius
|
||||
restoreButton.backgroundColor = Colors.accent
|
||||
restoreButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
restoreButton.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
restoreButton.setTitle(NSLocalizedString("session_reset_banner_restore_button_title", comment: ""), for: UIControl.State.normal)
|
||||
restoreButton.addTarget(self, action: #selector(restore), for: UIControl.Event.touchUpInside)
|
||||
// Set up dismiss button
|
||||
let dismissButton = UIButton()
|
||||
dismissButton.set(.height, to: Values.mediumButtonHeight)
|
||||
dismissButton.layer.cornerRadius = Values.modalButtonCornerRadius
|
||||
dismissButton.backgroundColor = Colors.buttonBackground
|
||||
dismissButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
dismissButton.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
dismissButton.setTitle(NSLocalizedString("session_reset_banner_dismiss_button_title", comment: ""), for: UIControl.State.normal)
|
||||
dismissButton.addTarget(self, action: #selector(dismiss), for: UIControl.Event.touchUpInside)
|
||||
// Set up button stack view
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ dismissButton, restoreButton ])
|
||||
buttonStackView.axis = .horizontal
|
||||
buttonStackView.spacing = Values.mediumSpacing
|
||||
buttonStackView.distribution = .fillEqually
|
||||
// Set up main stack view
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, buttonStackView ])
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.spacing = Values.smallSpacing
|
||||
addSubview(mainStackView)
|
||||
mainStackView.pin(to: self, withInset: Values.mediumSpacing)
|
||||
// Update explanation label if possible
|
||||
if let contactID = thread.contactIdentifier() {
|
||||
let displayName = UserDisplayNameUtilities.getPrivateChatDisplayName(for: contactID) ?? contactID
|
||||
explanationLabel.text = String(format: "Would you like to restore your session with %@? This can help resolve issues. Your messages will be preserved.", displayName)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func restore() { onRestore?() }
|
||||
|
||||
@objc private func dismiss() { onDismiss?() }
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import UIKit
|
||||
|
||||
final class UserCell : UITableViewCell {
|
||||
var accessory = Accessory.none
|
||||
var publicKey = ""
|
||||
|
||||
// MARK: Accessory
|
||||
enum Accessory {
|
||||
case none
|
||||
case tick(isSelected: Bool)
|
||||
}
|
||||
|
||||
// MARK: Components
|
||||
private lazy var profilePictureView = ProfilePictureView()
|
||||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var tickImageView: UIImageView = {
|
||||
let result = UIImageView()
|
||||
result.contentMode = .scaleAspectFit
|
||||
let size: CGFloat = 24
|
||||
result.set(.width, to: size)
|
||||
result.set(.height, to: size)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var separator: UIView = {
|
||||
let result = UIView()
|
||||
result.backgroundColor = Colors.separator
|
||||
result.set(.height, to: Values.separatorThickness)
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Initialization
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
// Set the cell background color
|
||||
backgroundColor = Colors.cellBackground
|
||||
// Set up the highlight color
|
||||
let selectedBackgroundView = UIView()
|
||||
selectedBackgroundView.backgroundColor = .clear // Disabled for now
|
||||
self.selectedBackgroundView = selectedBackgroundView
|
||||
// Set up the profile picture image view
|
||||
let profilePictureViewSize = Values.smallProfilePictureSize
|
||||
profilePictureView.set(.width, to: profilePictureViewSize)
|
||||
profilePictureView.set(.height, to: profilePictureViewSize)
|
||||
profilePictureView.size = profilePictureViewSize
|
||||
// Set up the main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel, tickImageView ])
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: Values.mediumSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.mediumSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.mediumSpacing)
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing)
|
||||
// Set up the separator
|
||||
contentView.addSubview(separator)
|
||||
separator.pin(.leading, to: .leading, of: contentView)
|
||||
contentView.pin(.trailing, to: .trailing, of: separator)
|
||||
separator.pin(.bottom, to: .bottom, of: contentView)
|
||||
separator.set(.width, to: UIScreen.main.bounds.width)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
func update() {
|
||||
profilePictureView.hexEncodedPublicKey = publicKey
|
||||
profilePictureView.update()
|
||||
displayNameLabel.text = UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) ?? publicKey
|
||||
switch accessory {
|
||||
case .none: tickImageView.isHidden = true
|
||||
case .tick(let isSelected):
|
||||
tickImageView.isHidden = false
|
||||
let icon = isSelected ? #imageLiteral(resourceName: "CircleCheck") : #imageLiteral(resourceName: "Circle")
|
||||
tickImageView.image = isDarkMode ? icon : icon.asTintedImage(color: Colors.text)!
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import SessionMessagingKit
|
||||
import SessionProtocolKit
|
||||
import SessionSnodeKit
|
||||
|
||||
@objc(SNConfiguration)
|
||||
final class Configuration : NSObject {
|
||||
|
||||
private static let pnServerURL = "https://live.apns.getsession.org"
|
||||
private static let pnServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
||||
|
||||
@objc static func performMainSetup() {
|
||||
SNMessagingKit.configure(
|
||||
storage: Storage.shared,
|
||||
signalStorage: OWSPrimaryStorage.shared(),
|
||||
identityKeyStore: OWSIdentityManager.shared(),
|
||||
sessionRestorationImplementation: SessionRestorationImplementation(),
|
||||
certificateValidator: SMKCertificateDefaultValidator(trustRoot: OWSUDManagerImpl.trustRoot()),
|
||||
openGroupAPIDelegate: UIApplication.shared.delegate as! AppDelegate,
|
||||
pnServerURL: pnServerURL,
|
||||
pnServerPublicKey: pnServerURL
|
||||
)
|
||||
SessionProtocolKit.configure(storage: Storage.shared, sharedSenderKeysDelegate: UIApplication.shared.delegate as! AppDelegate)
|
||||
SessionSnodeKit.configure(storage: Storage.shared)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
extension Storage : SessionMessagingKitStorageProtocol {
|
||||
|
||||
// MARK: Signal Protocol
|
||||
public func getOrGenerateRegistrationID(using transaction: Any) -> UInt32 {
|
||||
SSKEnvironment.shared.tsAccountManager.getOrGenerateRegistrationId(transaction as! YapDatabaseReadWriteTransaction)
|
||||
}
|
||||
|
||||
public func getSenderCertificate(for publicKey: String) -> SMKSenderCertificate {
|
||||
let (promise, seal) = Promise<SMKSenderCertificate>.pending()
|
||||
SSKEnvironment.shared.udManager.ensureSenderCertificate { senderCertificate in
|
||||
seal.fulfill(senderCertificate)
|
||||
} failure: { error in
|
||||
// Should never fail
|
||||
}
|
||||
return try! promise.wait()
|
||||
}
|
||||
|
||||
// MARK: Shared Sender Keys
|
||||
private static let closedGroupPrivateKeyCollection = "LokiClosedGroupPrivateKeyCollection"
|
||||
|
||||
public func getClosedGroupPrivateKey(for publicKey: String) -> String? {
|
||||
var result: String?
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: publicKey, inCollection: Storage.closedGroupPrivateKeyCollection) as? String
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
internal static func setClosedGroupPrivateKey(_ privateKey: String, for publicKey: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(privateKey, forKey: publicKey, inCollection: Storage.closedGroupPrivateKeyCollection)
|
||||
}
|
||||
|
||||
internal static func removeClosedGroupPrivateKey(for publicKey: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: publicKey, inCollection: Storage.closedGroupPrivateKeyCollection)
|
||||
}
|
||||
|
||||
func getUserClosedGroupPublicKeys() -> Set<String> {
|
||||
var result: Set<String> = []
|
||||
Storage.read { transaction in
|
||||
result = Set(transaction.allKeys(inCollection: Storage.closedGroupPrivateKeyCollection))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func isClosedGroup(_ publicKey: String) -> Bool {
|
||||
getUserClosedGroupPublicKeys().contains(publicKey)
|
||||
}
|
||||
|
||||
// MARK: Jobs
|
||||
public func persist(_ job: Job, using transaction: Any) { fatalError("Not implemented.") }
|
||||
public func markJobAsSucceeded(_ job: Job, using transaction: Any) { fatalError("Not implemented.") }
|
||||
public func markJobAsFailed(_ job: Job, using transaction: Any) { fatalError("Not implemented.") }
|
||||
|
||||
// MARK: Authorization
|
||||
private static func getAuthTokenCollection(for server: String) -> String {
|
||||
return (server == FileServerAPI.server) ? "LokiStorageAuthTokenCollection" : "LokiGroupChatAuthTokenCollection"
|
||||
}
|
||||
|
||||
public func getAuthToken(for server: String) -> String? {
|
||||
let collection = Storage.getAuthTokenCollection(for: server)
|
||||
var result: String? = nil
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: server, inCollection: collection) as? String
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setAuthToken(for server: String, to newValue: String, using transaction: Any) {
|
||||
let collection = Storage.getAuthTokenCollection(for: server)
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: server, inCollection: collection)
|
||||
}
|
||||
|
||||
public func removeAuthToken(for server: String, using transaction: Any) {
|
||||
let collection = Storage.getAuthTokenCollection(for: server)
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: server, inCollection: collection)
|
||||
}
|
||||
|
||||
// MARK: Open Group Public Keys
|
||||
private static let openGroupPublicKeyCollection = "LokiOpenGroupPublicKeyCollection"
|
||||
|
||||
public func getOpenGroupPublicKey(for server: String) -> String? {
|
||||
var result: String? = nil
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: server, inCollection: Storage.openGroupPublicKeyCollection) as? String
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setOpenGroupPublicKey(for server: String, to newValue: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: server, inCollection: Storage.openGroupPublicKeyCollection)
|
||||
}
|
||||
|
||||
// MARK: Last Message Server ID
|
||||
private static let lastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection"
|
||||
|
||||
public func getLastMessageServerID(for group: UInt64, on server: String) -> UInt64? {
|
||||
var result: UInt64? = nil
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: "\(server).\(group)", inCollection: Storage.lastMessageServerIDCollection) as? UInt64
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setLastMessageServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: "\(server).\(group)", inCollection: Storage.lastMessageServerIDCollection)
|
||||
}
|
||||
|
||||
public func removeLastMessageServerID(for group: UInt64, on server: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: "\(server).\(group)", inCollection: Storage.lastMessageServerIDCollection)
|
||||
}
|
||||
|
||||
// MARK: Last Deletion Server ID
|
||||
private static let lastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection"
|
||||
|
||||
public func getLastDeletionServerID(for group: UInt64, on server: String) -> UInt64? {
|
||||
var result: UInt64? = nil
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: "\(server).\(group)", inCollection: Storage.lastDeletionServerIDCollection) as? UInt64
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setLastDeletionServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: "\(server).\(group)", inCollection: Storage.lastDeletionServerIDCollection)
|
||||
}
|
||||
|
||||
public func removeLastDeletionServerID(for group: UInt64, on server: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: "\(server).\(group)", inCollection: Storage.lastDeletionServerIDCollection)
|
||||
}
|
||||
|
||||
// MARK: Open Group Metadata
|
||||
private static let openGroupUserCountCollection = "LokiPublicChatUserCountCollection"
|
||||
private static let openGroupMessageIDCollection = "LKMessageIDCollection"
|
||||
|
||||
public func setUserCount(to newValue: Int, forOpenGroupWithID openGroupID: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: openGroupID, inCollection: Storage.openGroupUserCountCollection)
|
||||
}
|
||||
|
||||
public func getIDForMessage(withServerID serverID: UInt64) -> UInt64? {
|
||||
var result: UInt64? = nil
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: String(serverID), inCollection: Storage.openGroupMessageIDCollection) as? UInt64
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setOpenGroupDisplayName(to displayName: String, for publicKey: String, on channel: UInt64, server: String, using transaction: Any) {
|
||||
let collection = "\(server).\(channel)" // FIXME: This should be a proper collection
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(displayName, forKey: publicKey, inCollection: collection)
|
||||
}
|
||||
|
||||
public func setLastProfilePictureUploadDate(_ date: Date) {
|
||||
UserDefaults.standard[.lastProfilePictureUpload] = date
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
extension Storage : SessionProtocolKitStorageProtocol {
|
||||
|
||||
private func getClosedGroupRatchetCollection(_ collection: ClosedGroupRatchetCollectionType, for groupPublicKey: String) -> String {
|
||||
switch collection {
|
||||
case .old: return "LokiOldClosedGroupRatchetCollection.\(groupPublicKey)"
|
||||
case .current: return "LokiClosedGroupRatchetCollection.\(groupPublicKey)"
|
||||
}
|
||||
}
|
||||
|
||||
public func getClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> ClosedGroupRatchet? {
|
||||
let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey)
|
||||
var result: ClosedGroupRatchet?
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: senderPublicKey, inCollection: collection) as? ClosedGroupRatchet
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, in collection: ClosedGroupRatchetCollectionType = .current, using transaction: Any) {
|
||||
let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey)
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(ratchet, forKey: senderPublicKey, inCollection: collection)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
|
||||
extension Storage : SessionSnodeKitStorageProtocol {
|
||||
|
||||
// MARK: Onion Request Paths
|
||||
private static let onionRequestPathCollection = "LokiOnionRequestPathCollection"
|
||||
|
||||
public func getOnionRequestPaths() -> [OnionRequestAPI.Path] {
|
||||
let collection = Storage.onionRequestPathCollection
|
||||
var result: [OnionRequestAPI.Path] = []
|
||||
Storage.read { transaction in
|
||||
if
|
||||
let path0Snode0 = transaction.object(forKey: "0-0", inCollection: collection) as? Snode,
|
||||
let path0Snode1 = transaction.object(forKey: "0-1", inCollection: collection) as? Snode,
|
||||
let path0Snode2 = transaction.object(forKey: "0-2", inCollection: collection) as? Snode {
|
||||
result.append([ path0Snode0, path0Snode1, path0Snode2 ])
|
||||
if
|
||||
let path1Snode0 = transaction.object(forKey: "1-0", inCollection: collection) as? Snode,
|
||||
let path1Snode1 = transaction.object(forKey: "1-1", inCollection: collection) as? Snode,
|
||||
let path1Snode2 = transaction.object(forKey: "1-2", inCollection: collection) as? Snode {
|
||||
result.append([ path1Snode0, path1Snode1, path1Snode2 ])
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setOnionRequestPaths(to paths: [OnionRequestAPI.Path], using transaction: Any) {
|
||||
let collection = Storage.onionRequestPathCollection
|
||||
// FIXME: This approach assumes either 1 or 2 paths of length 3 each. We should do better than this.
|
||||
clearOnionRequestPaths(using: transaction)
|
||||
guard let transaction = transaction as? YapDatabaseReadWriteTransaction else { return }
|
||||
guard paths.count >= 1 else { return }
|
||||
let path0 = paths[0]
|
||||
guard path0.count == 3 else { return }
|
||||
transaction.setObject(path0[0], forKey: "0-0", inCollection: collection)
|
||||
transaction.setObject(path0[1], forKey: "0-1", inCollection: collection)
|
||||
transaction.setObject(path0[2], forKey: "0-2", inCollection: collection)
|
||||
guard paths.count >= 2 else { return }
|
||||
let path1 = paths[1]
|
||||
guard path1.count == 3 else { return }
|
||||
transaction.setObject(path1[0], forKey: "1-0", inCollection: collection)
|
||||
transaction.setObject(path1[1], forKey: "1-1", inCollection: collection)
|
||||
transaction.setObject(path1[2], forKey: "1-2", inCollection: collection)
|
||||
}
|
||||
|
||||
func clearOnionRequestPaths(using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeAllObjects(inCollection: Storage.onionRequestPathCollection)
|
||||
}
|
||||
|
||||
// MARK: Snode Pool
|
||||
public func getSnodePool() -> Set<Snode> {
|
||||
var result: Set<Snode> = []
|
||||
Storage.read { transaction in
|
||||
transaction.enumerateKeysAndObjects(inCollection: Storage.snodePoolCollection) { _, object, _ in
|
||||
guard let snode = object as? Snode else { return }
|
||||
result.insert(snode)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setSnodePool(to snodePool: Set<Snode>, using transaction: Any) {
|
||||
clearSnodePool(in: transaction)
|
||||
snodePool.forEach { snode in
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(snode, forKey: snode.description, inCollection: Storage.snodePoolCollection)
|
||||
}
|
||||
}
|
||||
|
||||
func clearSnodePool(in transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeAllObjects(inCollection: Storage.snodePoolCollection)
|
||||
}
|
||||
|
||||
// MARK: Swarm
|
||||
public func getSwarm(for publicKey: String) -> Set<Snode> {
|
||||
var result: Set<Snode> = []
|
||||
let collection = Storage.getSwarmCollection(for: publicKey)
|
||||
Storage.read { transaction in
|
||||
transaction.enumerateKeysAndObjects(inCollection: collection) { _, object, _ in
|
||||
guard let snode = object as? Snode else { return }
|
||||
result.insert(snode)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setSwarm(to swarm: Set<Snode>, for publicKey: String, using transaction: Any) {
|
||||
clearSwarm(for: publicKey, in: transaction)
|
||||
let collection = Storage.getSwarmCollection(for: publicKey)
|
||||
swarm.forEach { snode in
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(snode, forKey: snode.description, inCollection: collection)
|
||||
}
|
||||
}
|
||||
|
||||
func clearSwarm(for publicKey: String, in transaction: Any) {
|
||||
let collection = Storage.getSwarmCollection(for: publicKey)
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeAllObjects(inCollection: collection)
|
||||
}
|
||||
|
||||
// MARK: Last Message Hash
|
||||
private static let lastMessageHashCollection = "LokiLastMessageHashCollection"
|
||||
|
||||
func getLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String) -> JSON? {
|
||||
let key = "\(snode.address):\(snode.port).\(publicKey)"
|
||||
var result: JSON?
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: key, inCollection: Storage.lastMessageHashCollection) as? JSON
|
||||
}
|
||||
if let result = result {
|
||||
guard result["hash"] as? String != nil else { return nil }
|
||||
guard result["expirationDate"] as? NSNumber != nil else { return nil }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func getLastMessageHash(for snode: Snode, associatedWith publicKey: String) -> String? {
|
||||
return getLastMessageHashInfo(for: snode, associatedWith: publicKey)?["hash"] as? String
|
||||
}
|
||||
|
||||
public func setLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any) {
|
||||
let key = "\(snode.address):\(snode.port).\(publicKey)"
|
||||
guard lastMessageHashInfo.count == 2 && lastMessageHashInfo["hash"] as? String != nil && lastMessageHashInfo["expirationDate"] as? NSNumber != nil else { return }
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(lastMessageHashInfo, forKey: key, inCollection: Storage.lastMessageHashCollection)
|
||||
}
|
||||
|
||||
public func pruneLastMessageHashInfoIfExpired(for snode: Snode, associatedWith publicKey: String, using transaction: Any) {
|
||||
guard let lastMessageHashInfo = getLastMessageHashInfo(for: snode, associatedWith: publicKey),
|
||||
(lastMessageHashInfo["hash"] as? String) != nil, let expirationDate = (lastMessageHashInfo["expirationDate"] as? NSNumber)?.uint64Value else { return }
|
||||
let now = NSDate.millisecondTimestamp()
|
||||
if now >= expirationDate {
|
||||
removeLastMessageHashInfo(for: snode, associatedWith: publicKey, using: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
func removeLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, using transaction: Any) {
|
||||
let key = "\(snode.address):\(snode.port).\(publicKey)"
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: Storage.lastMessageHashCollection)
|
||||
}
|
||||
|
||||
// MARK: Received Messages
|
||||
private static let receivedMessagesCollection = "LokiReceivedMessagesCollection"
|
||||
|
||||
public func getReceivedMessages(for publicKey: String) -> Set<String> {
|
||||
var result: Set<String>?
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: publicKey, inCollection: Storage.receivedMessagesCollection) as? Set<String>
|
||||
}
|
||||
return result ?? []
|
||||
}
|
||||
|
||||
public func setReceivedMessages(to receivedMessages: Set<String>, for publicKey: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(receivedMessages, forKey: publicKey, inCollection: Storage.receivedMessagesCollection)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
extension Storage {
|
||||
|
||||
public static let shared = Storage()
|
||||
|
||||
public func with(_ work: @escaping (Any) -> Void) {
|
||||
Storage.writeSync { work($0) }
|
||||
}
|
||||
|
||||
public func withAsync(_ work: @escaping (Any) -> Void, completion: @escaping () -> Void) {
|
||||
Storage.write(with: { work($0) }, completion: completion)
|
||||
}
|
||||
|
||||
public func getUserPublicKey() -> String? {
|
||||
return OWSIdentityManager.shared().identityKeyPair()?.publicKey.toHexString()
|
||||
}
|
||||
|
||||
public func getUserKeyPair() -> ECKeyPair? {
|
||||
return OWSIdentityManager.shared().identityKeyPair()
|
||||
}
|
||||
|
||||
public func getUserDisplayName() -> String? { fatalError() }
|
||||
}
|
Can't render this file because it is too large.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue