Merge pull request #311 from loki-project/refactor-3

Refactoring Part 2
This commit is contained in:
Niels Andriesse 2020-11-13 10:09:10 +11:00 committed by GitHub
commit e34e1f0392
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2842 changed files with 164467 additions and 162193 deletions

2
.gitignore vendored
View File

@ -28,3 +28,5 @@ DerivedData
*.xcuserstate
Index/
# CocoaPods
Pods

3
.gitmodules vendored
View File

@ -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

View File

@ -1 +0,0 @@
2.6.0

View File

@ -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

View File

@ -1,5 +0,0 @@
source 'https://rubygems.org'
gem 'cocoapods'
gem 'fastlane'

View File

@ -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

56
Jenkinsfile vendored
View File

@ -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}")
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}
}
*/

View File

@ -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)
}
}

View File

@ -1,9 +0,0 @@
import PromiseKit
@testable import SignalServiceKit
import XCTest
class SessionManagementProtocolTests : XCTestCase {
// TODO: Add tests
}

View File

@ -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
}
}
*/

View File

@ -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()
}
}
}

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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) { }
}

View File

@ -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
View File

@ -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

View File

@ -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 +0,0 @@
Subproject commit d1b2c2c2fe1b47ab1314192e9320f6cbc30871be

View File

@ -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)

View File

@ -1,3 +0,0 @@
#!/bin/bash
rm -rf ~/Library/Developer/Xcode/DerivedData

View File

@ -1,4 +0,0 @@
#!/bin/bash
git reset --hard HEAD
git clean -xdff

View File

@ -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)

View File

@ -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

View File

@ -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 workers 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 oclock..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 oclock..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 oclock..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

View File

@ -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

View File

@ -1 +0,0 @@
Copy these git hooks into .git/hooks

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
Scripts/reverse_integration_check.py

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
Scripts/precommit.py

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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)
}
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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?() }
}

View File

@ -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)!
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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() }
}

Some files were not shown because too many files have changed in this diff Show More