diff --git a/Signal/src/Loki/View Controllers/HomeVC.swift b/Signal/src/Loki/View Controllers/HomeVC.swift index 8d63bbf1d..585042111 100644 --- a/Signal/src/Loki/View Controllers/HomeVC.swift +++ b/Signal/src/Loki/View Controllers/HomeVC.swift @@ -415,9 +415,21 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol transaction.removeObject(forKey: "\(publicChat.server).\(publicChat.channel)", inCollection: PublicChatAPI.lastMessageServerIDCollection) transaction.removeObject(forKey: "\(publicChat.server).\(publicChat.channel)", inCollection: PublicChatAPI.lastDeletionServerIDCollection) let _ = PublicChatAPI.leave(publicChat.channel, on: publicChat.server) + thread.removeAllThreadInteractions(with: transaction) + thread.remove(with: transaction) + } else if let thread = thread as? TSGroupThread, thread.usesSharedSenderKeys == true { + let groupID = thread.groupModel.groupId + let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID) + let _ = ClosedGroupsProtocol.leave(groupPublicKey, using: transaction).ensure { + try! Storage.writeSync { transaction in + thread.removeAllThreadInteractions(with: transaction) + thread.remove(with: transaction) + } + } + } else { + thread.removeAllThreadInteractions(with: transaction) + thread.remove(with: transaction) } - thread.removeAllThreadInteractions(with: transaction) - thread.remove(with: transaction) } NotificationCenter.default.post(name: .threadDeleted, object: nil, userInfo: [ "threadId" : thread.uniqueId! ]) }) diff --git a/Signal/src/Loki/View Controllers/SettingsVC.swift b/Signal/src/Loki/View Controllers/SettingsVC.swift index 5c354a70c..9f93aed86 100644 --- a/Signal/src/Loki/View Controllers/SettingsVC.swift +++ b/Signal/src/Loki/View Controllers/SettingsVC.swift @@ -128,6 +128,12 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate { stackView.pin(to: scrollView) view.addSubview(scrollView) scrollView.pin(to: view) + // Register for notifications + NotificationCenter.default.addObserver(self, selector: #selector(handleAppModeSwitchedNotification(_:)), name: .appModeSwitched, object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) } private func getSettingButtons() -> [UIView] { @@ -203,6 +209,11 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate { } // MARK: Updating + @objc private func handleAppModeSwitchedNotification(_ notification: Notification) { + updateNavigationBarButtons() + // TODO: Redraw UI + } + private func handleIsEditingDisplayNameChanged() { updateNavigationBarButtons() UIView.animate(withDuration: 0.25) { @@ -228,9 +239,12 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate { let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close)) closeButton.tintColor = Colors.text navigationItem.leftBarButtonItem = closeButton + let appModeIcon = UserDefaults.standard[.isUsingDarkMode] ? #imageLiteral(resourceName: "ic_dark_theme_on") : #imageLiteral(resourceName: "ic_dark_theme_off") + let appModeButton = UIBarButtonItem(image: appModeIcon, style: .plain, target: self, action: #selector(switchAppMode)) + appModeButton.tintColor = Colors.text let qrCodeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "QRCode"), style: .plain, target: self, action: #selector(showQRCode)) qrCodeButton.tintColor = Colors.text - navigationItem.rightBarButtonItem = qrCodeButton + navigationItem.rightBarButtonItems = [ qrCodeButton/*, appModeButton*/ ] } } @@ -281,7 +295,13 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate { @objc private func close() { dismiss(animated: true, completion: nil) } - + + @objc private func switchAppMode() { + let isUsingDarkMode = UserDefaults.standard[.isUsingDarkMode] + UserDefaults.standard[.isUsingDarkMode] = !isUsingDarkMode + NotificationCenter.default.post(name: .appModeSwitched, object: nil) + } + @objc private func showQRCode() { let qrCodeVC = QRCodeVC() navigationController!.pushViewController(qrCodeVC, animated: true) diff --git a/Signal/src/ViewControllers/CropScaleImageViewController.swift b/Signal/src/ViewControllers/CropScaleImageViewController.swift index 5f742c096..2ba1ae0fe 100644 --- a/Signal/src/ViewControllers/CropScaleImageViewController.swift +++ b/Signal/src/ViewControllers/CropScaleImageViewController.swift @@ -191,7 +191,7 @@ import SignalMessaging maskingView.autoPinEdgesToSuperviewEdges() let titleLabel = UILabel() - titleLabel.textColor = Colors.text + titleLabel.textColor = .white titleLabel.textAlignment = .center titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) titleLabel.text = NSLocalizedString("CROP_SCALE_IMAGE_VIEW_TITLE", diff --git a/Signal/src/ViewControllers/LongTextViewController.swift b/Signal/src/ViewControllers/LongTextViewController.swift index 6383acf48..841e7dd87 100644 --- a/Signal/src/ViewControllers/LongTextViewController.swift +++ b/Signal/src/ViewControllers/LongTextViewController.swift @@ -171,7 +171,7 @@ public class LongTextViewController: OWSViewController { view.addSubview(footer) footer.autoPinWidthToSuperview() footer.autoPinEdge(.top, to: .bottom, of: messageTextView) - footer.autoPinEdge(.bottom, to: .bottom, of: view) + footer.autoPinEdge(toSuperviewSafeArea: .bottom) footer.items = [ UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), diff --git a/SignalMessaging/Loki/Redesign/Style Guide/AppMode.swift b/SignalMessaging/Loki/Redesign/Style Guide/AppMode.swift index ee386e790..26fadb3d5 100644 --- a/SignalMessaging/Loki/Redesign/Style Guide/AppMode.swift +++ b/SignalMessaging/Loki/Redesign/Style Guide/AppMode.swift @@ -3,6 +3,9 @@ public enum AppMode { case light, dark public static var current: AppMode = .dark +// public static var current: AppMode { +// return UserDefaults.standard[.isUsingDarkMode] ? .dark : .light +// } } public var isLightMode: Bool { diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift index 345bd8806..b96eddd53 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift @@ -290,7 +290,8 @@ public enum OnionRequestAPI { let url = "\(guardSnode.address):\(guardSnode.port)/onion_req" let finalEncryptionResult = intermediate.finalEncryptionResult let onion = finalEncryptionResult.ciphertext - if case Destination.server = destination, Double(onion.count) > 0.75 * (Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier) { + let requestSizeLimit = Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier + if case Destination.server = destination, Double(onion.count) > 0.75 * requestSizeLimit { print("[Loki] Approaching request size limit: ~\(onion.count) bytes.") } let parameters: JSON = [ diff --git a/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift b/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift index 0aa9d5a89..8c233519a 100644 --- a/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift +++ b/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift @@ -124,22 +124,25 @@ public final class ClosedGroupsProtocol : NSObject { infoMessage.save(with: transaction) } - public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey() - removeMembers([ userPublicKey ], from: groupPublicKey, using: transaction) + return removeMembers([ userPublicKey ], from: groupPublicKey, using: transaction) } - public static func removeMembers(_ membersToRemove: Set, from groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + /// The returned promise is fulfilled when the message has been sent **to the group**. It doesn't wait for the user's new ratchet to be distributed. + public static func removeMembers(_ membersToRemove: Set, from groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { // Prepare let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey() let isUserLeaving = membersToRemove.contains(userPublicKey) guard !isUserLeaving || membersToRemove.count == 1 else { - return print("[Loki] Can't remove self and others simultaneously.") + print("[Loki] Can't remove self and others simultaneously.") + return Promise.value(()) } let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) guard let thread = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) else { - return print("[Loki] Can't remove users from nonexistent closed group.") + print("[Loki] Can't remove users from nonexistent closed group.") + return Promise.value(()) } let group = thread.groupModel let name = group.groupName! @@ -149,7 +152,8 @@ public final class ClosedGroupsProtocol : NSObject { var members = group.groupMemberIds let indexes = membersToRemove.compactMap { members.firstIndex(of: $0) } guard indexes.count == membersToRemove.count else { - return print("[Loki] Can't remove users from group.") + print("[Loki] Can't remove users from group.") + return Promise.value(()) } indexes.forEach { members.remove(at: $0) } let membersAsData = members.map { Data(hex: $0) } @@ -157,14 +161,20 @@ public final class ClosedGroupsProtocol : NSObject { let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: [], members: membersAsData, admins: adminsAsData) let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) - messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction) - // Delete all ratchets (it's important that this happens after sending out the update) - Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction) - // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and send it out to all - // members (minus the removed ones) and their linked devices using established channels. - if isUserLeaving { - Storage.removeClosedGroupPrivateKey(for: groupPublicKey, using: transaction) - } else { + let (promise, seal) = Promise.pending() + SSKEnvironment.shared.messageSender.send(closedGroupUpdateMessage, success: { seal.fulfill(()) }, failure: { seal.reject($0) }) + promise.done { + try! Storage.writeSync { transaction in + // Delete all ratchets (it's important that this happens after sending out the update) + Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction) + // Remove the group from the user's set of public keys to poll for + if isUserLeaving { + Storage.removeClosedGroupPrivateKey(for: groupPublicKey, using: transaction) + } + } + } + // Generate a new ratchet and send it out to all members (minus the removed ones) and their linked devices using established channels if needed. + if !isUserLeaving { // Establish sessions if needed establishSessionsIfNeeded(with: members, using: transaction) // This internally takes care of multi device // Send out the user's new ratchet to all members (minus the removed ones) and their linked devices using established channels @@ -186,6 +196,8 @@ public final class ClosedGroupsProtocol : NSObject { let infoMessageType: TSInfoMessageType = isUserLeaving ? .typeGroupQuit : .typeGroupUpdate let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: infoMessageType) infoMessage.save(with: transaction) + // Return + return promise } public static func requestSenderKey(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { @@ -274,7 +286,7 @@ public final class ClosedGroupsProtocol : NSObject { } let group = thread.groupModel // Check that the sender is a member of the group (before the update) - var membersAndLinkedDevices: Set = [] + var membersAndLinkedDevices: Set = Set(group.groupMemberIds) for member in group.groupMemberIds { let deviceLinks = OWSPrimaryStorage.shared().getDeviceLinks(for: member, in: transaction) membersAndLinkedDevices.formUnion(deviceLinks.flatMap { [ $0.master.publicKey, $0.slave.publicKey ] }) @@ -333,7 +345,7 @@ public final class ClosedGroupsProtocol : NSObject { } let group = groupThread.groupModel // Check that the requesting user is a member of the group - var membersAndLinkedDevices: Set = [] + var membersAndLinkedDevices: Set = Set(group.groupMemberIds) for member in group.groupMemberIds { let deviceLinks = OWSPrimaryStorage.shared().getDeviceLinks(for: member, in: transaction) membersAndLinkedDevices.formUnion(deviceLinks.flatMap { [ $0.master.publicKey, $0.slave.publicKey ] }) @@ -365,7 +377,7 @@ public final class ClosedGroupsProtocol : NSObject { return print("[Loki] Ignoring invalid closed group sender key.") } // Check that the requesting user is a member of the group - var membersAndLinkedDevices: Set = [] + var membersAndLinkedDevices: Set = Set(group.groupMemberIds) for member in group.groupMemberIds { let deviceLinks = OWSPrimaryStorage.shared().getDeviceLinks(for: member, in: transaction) membersAndLinkedDevices.formUnion(deviceLinks.flatMap { [ $0.master.publicKey, $0.slave.publicKey ] }) diff --git a/SignalServiceKit/src/Loki/Protocol/Utilities/LKGroupUtilities.m b/SignalServiceKit/src/Loki/Protocol/Utilities/LKGroupUtilities.m index 38cdca4c6..2cc376243 100644 --- a/SignalServiceKit/src/Loki/Protocol/Utilities/LKGroupUtilities.m +++ b/SignalServiceKit/src/Loki/Protocol/Utilities/LKGroupUtilities.m @@ -1,4 +1,6 @@ #import "LKGroupUtilities.h" +#import "NSData+OWS.h" +#import @implementation LKGroupUtilities @@ -64,8 +66,12 @@ +(NSData *)getDecodedGroupIDAsData:(NSData *)groupID { + // FIXME: This needs to be cleaned up. A lot. OWSAssertDebug(groupID.length > 0); - NSString *encodedGroupID = [[NSString alloc]initWithData:groupID encoding:NSUTF8StringEncoding]; + NSString *encodedGroupID = [[NSString alloc] initWithData:groupID encoding:NSUTF8StringEncoding]; + if (encodedGroupID == nil && [ECKeyPair isValidHexEncodedPublicKeyWithCandidate:[groupID hexadecimalString]]) { + return groupID; // Workaround to make things compatible with Android + } NSString *decodedGroupID = [encodedGroupID componentsSeparatedByString:@"!"][0]; if ([encodedGroupID componentsSeparatedByString:@"!"].count > 1) { decodedGroupID = [encodedGroupID componentsSeparatedByString:@"!"][1]; diff --git a/SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift b/SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift index 21ab9158d..7023b3a49 100644 --- a/SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift +++ b/SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift @@ -10,6 +10,7 @@ public enum LKUserDefaults { case hasViewedSeed /// Whether the device was unlinked as a slave device (used to notify the user on the landing screen). case wasUnlinked + case isUsingDarkMode case isUsingFullAPNs } diff --git a/SignalServiceKit/src/Loki/Utilities/Notification+Loki.swift b/SignalServiceKit/src/Loki/Utilities/Notification+Loki.swift index da6894d6d..1385a044a 100644 --- a/SignalServiceKit/src/Loki/Utilities/Notification+Loki.swift +++ b/SignalServiceKit/src/Loki/Utilities/Notification+Loki.swift @@ -2,6 +2,7 @@ public extension Notification.Name { // State changes + public static let appModeSwitched = Notification.Name("appModeSwitched") public static let blockedContactsUpdated = Notification.Name("blockedContactsUpdated") public static let contactOnlineStatusChanged = Notification.Name("contactOnlineStatusChanged") public static let groupThreadUpdated = Notification.Name("groupThreadUpdated") @@ -28,6 +29,7 @@ public extension Notification.Name { @objc public extension NSNotification { // State changes + @objc public static let appModeSwitched = Notification.Name.appModeSwitched.rawValue as NSString @objc public static let blockedContactsUpdated = Notification.Name.blockedContactsUpdated.rawValue as NSString @objc public static let contactOnlineStatusChanged = Notification.Name.contactOnlineStatusChanged.rawValue as NSString @objc public static let groupThreadUpdated = Notification.Name.groupThreadUpdated.rawValue as NSString