diff --git a/Pods b/Pods index 79eaca77d..fd594cfe7 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 79eaca77d2f8b7aee0907f0610804682c41c8a4c +Subproject commit fd594cfe7694dd9b568254eaa9551ed18366ae0e diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 96fb745af..aeb17a13a 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 2400888E239F30A600305217 /* SessionRestoreBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2400888D239F30A600305217 /* SessionRestoreBannerView.swift */; }; 241C6314231F64C000B4198E /* JazzIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C630E231F5AAC00B4198E /* JazzIcon.swift */; }; 241C6315231F64CE00B4198E /* CGFloat+Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C6312231F5F1D00B4198E /* CGFloat+Rounding.swift */; }; 241C6316231F64CE00B4198E /* UIColor+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C6310231F5C4400B4198E /* UIColor+Helper.swift */; }; @@ -681,6 +682,7 @@ 0F94C85CB0B235DA37F68ED0 /* Pods_SignalShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1C93CF3971B64E8B6C1F9AC1 /* Pods-SignalShareExtension.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.test.xcconfig"; sourceTree = ""; }; 1CE3CD5C23334683BDD3D78C /* Pods-Signal.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.test.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.test.xcconfig"; sourceTree = ""; }; + 2400888D239F30A600305217 /* SessionRestoreBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRestoreBannerView.swift; sourceTree = ""; }; 241C630E231F5AAC00B4198E /* JazzIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JazzIcon.swift; sourceTree = ""; }; 241C6310231F5C4400B4198E /* UIColor+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Helper.swift"; sourceTree = ""; }; 241C6312231F5F1D00B4198E /* CGFloat+Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Rounding.swift"; sourceTree = ""; }; @@ -2717,6 +2719,7 @@ B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */, 24BD2608234DA2050008EB0A /* NewPublicChatVC.swift */, B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */, + 2400888D239F30A600305217 /* SessionRestoreBannerView.swift */, ); path = Messaging; sourceTree = ""; @@ -3850,6 +3853,7 @@ B8162F0322891AD600D46544 /* FriendRequestView.swift in Sources */, 458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */, 34B6A905218B4C91007C4606 /* TypingIndicatorInteraction.swift in Sources */, + 2400888E239F30A600305217 /* SessionRestoreBannerView.swift in Sources */, 4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */, 34EA69402194933900702471 /* MediaDownloadView.swift in Sources */, 340FC8AB204DAC8D007AEB0F /* DomainFrontingCountryViewController.m in Sources */, diff --git a/Signal/src/Loki/Messaging/SessionRestoreBannerView.swift b/Signal/src/Loki/Messaging/SessionRestoreBannerView.swift new file mode 100644 index 000000000..fb9d683a9 --- /dev/null +++ b/Signal/src/Loki/Messaging/SessionRestoreBannerView.swift @@ -0,0 +1,83 @@ +@objc(LKSessionRestoreBannerView) +final class SessionRestoreBannerView : UIView { + private let thread: TSThread + @objc public var onRestore: (() -> Void)? + @objc public var onDismiss: (() -> Void)? + + private lazy var bannerView: UIView = { + let bannerView = UIView.container() + bannerView.backgroundColor = UIColor.lokiGray() + bannerView.layer.cornerRadius = 2.5; + + // Use a shadow to "pop" the indicator above the other views. + bannerView.layer.shadowColor = UIColor.black.cgColor + bannerView.layer.shadowOffset = CGSize(width: 2, height: 3) + bannerView.layer.shadowRadius = 2 + bannerView.layer.shadowOpacity = 0.35 + return bannerView + }() + + private lazy var label: UILabel = { + let result = UILabel() + result.textColor = UIColor.white + result.font = UIFont.ows_dynamicTypeSubheadlineClamped + result.numberOfLines = 0 + result.textAlignment = .center + result.lineBreakMode = .byWordWrapping + return result + }() + + private lazy var buttonStackView: UIStackView = { + let result = UIStackView() + result.axis = .horizontal + result.distribution = .fillEqually + return result + }() + + private lazy var buttonFont = UIFont.ows_dynamicTypeBodyClamped.ows_mediumWeight() + private lazy var buttonHeight = buttonFont.pointSize * 48 / 17 + + // MARK: Lifecycle + @objc init(thread: TSThread) { + self.thread = thread; + super.init(frame: CGRect.zero) + initialize() + } + + required init?(coder: NSCoder) { fatalError("Using SessionRestoreBannerView.init(coder:) isn't allowed. Use SessionRestoreBannerView.init(thread:) instead.") } + override init(frame: CGRect) { fatalError("Using SessionRestoreBannerView.init(frame:) isn't allowed. Use SessionRestoreBannerView.init(thread:) instead.") } + + private func initialize() { + // Set up UI + let mainStackView = UIStackView() + mainStackView.axis = .vertical + mainStackView.distribution = .fill + mainStackView.addArrangedSubview(label) + mainStackView.addArrangedSubview(buttonStackView) + + let restoreButton = OWSFlatButton.button(title: NSLocalizedString("Restore session", comment: ""), font: buttonFont, titleColor: .ows_materialBlue, backgroundColor: .white, target: self, selector:#selector(restore)) + restoreButton.setBackgroundColors(upColor: .clear, downColor: .clear) + restoreButton.autoSetDimension(.height, toSize: buttonHeight) + buttonStackView.addArrangedSubview(restoreButton) + + let dismissButton = OWSFlatButton.button(title: NSLocalizedString("DISMISS_BUTTON_TEXT", comment: ""), font: buttonFont, titleColor: .ows_white, backgroundColor: .white, target: self, selector:#selector(dismiss)) + dismissButton.setBackgroundColors(upColor: .clear, downColor: .clear) + dismissButton.autoSetDimension(.height, toSize: buttonHeight) + buttonStackView.addArrangedSubview(dismissButton) + + bannerView.addSubview(mainStackView) + mainStackView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 16, left: 16, bottom: 8, right: 16)) + + addSubview(bannerView) + bannerView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)) + + if let contactID = thread.contactIdentifier() { + let displayName = Environment.shared.contactsManager.profileName(forRecipientId: contactID) ?? contactID + label.text = String(format: NSLocalizedString("Would you like to start a new session with %@?", comment: ""), displayName) + } + } + + @objc private func restore() { onRestore?() } + + @objc private func dismiss() { onDismiss?() } +} diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m index b79a96acc..72aa6922c 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m @@ -431,12 +431,8 @@ typedef void (^SystemMessageActionBlock)(void); }]; case TSErrorMessageMissingKeyId: case TSErrorMessageNoSession: - return nil; case TSErrorMessageInvalidMessage: - return [SystemMessageAction actionWithTitle:NSLocalizedString(@"FINGERPRINT_SHRED_KEYMATERIAL_BUTTON", @"") - block:^{ - [weakSelf.delegate tappedCorruptedMessage:message]; - }]; + return nil; case TSErrorMessageDuplicateMessage: case TSErrorMessageInvalidVersion: return nil; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 58554ad63..cb33c845a 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -803,6 +803,7 @@ typedef enum : NSUInteger { OWSLogDebug(@"viewWillAppear"); [self ensureBannerState]; + [self updateSessionRestoreBanner]; [super viewWillAppear:animated]; @@ -967,8 +968,20 @@ typedef enum : NSUInteger { if (isContactThread) { TSContactThread *thread = (TSContactThread *)self.thread; if (thread.sessionRestoreDevices.count > 0) { - if (self.restoreSessionBannerView) { - // TODO: Create banner here + if (!self.restoreSessionBannerView) { + LKSessionRestoreBannerView *bannerView = [[LKSessionRestoreBannerView alloc] initWithThread:thread]; + [self.view addSubview:bannerView]; + [bannerView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [bannerView autoPinEdgeToSuperviewEdge:ALEdgeLeft]; + [bannerView autoPinEdgeToSuperviewEdge:ALEdgeRight]; + [self.view layoutSubviews]; + self.restoreSessionBannerView = bannerView; + [bannerView setOnRestore:^{ + [self restoreSession]; + }]; + [bannerView setOnDismiss:^{ + [thread removeAllRessionRestoreDevicesWithTransaction:nil]; + }]; } } else { shouldRemoveBanner = true; @@ -1154,10 +1167,21 @@ typedef enum : NSUInteger { - (void)restoreSession { if ([self.thread isKindOfClass:[TSContactThread class]]) { + OWSMessageSender *messageSender = SSKEnvironment.shared.messageSender; TSContactThread *thread = (TSContactThread *)self.thread; NSArray *devices = thread.sessionRestoreDevices; - // TODO: Send session restore to all devices - // TODO: Add message saying session restore was sent + for (NSString *device in devices) { + if (device.length == 0) { continue; } + OWSMessageSend *sessionRestoreMessage = [messageSender getSessionRestoreMessageForHexEncodedPublicKey:device]; + if (sessionRestoreMessage) { + dispatch_async(OWSDispatch.sendingQueue, ^{ + [messageSender sendMessage:sessionRestoreMessage]; + }); + } + } + [[[TSInfoMessage alloc] initWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageType:TSInfoMessageTypeLokiSessionResetInProgress] save]; + thread.sessionResetState = TSContactThreadSessionResetStateRequestReceived; + [thread save]; [thread removeAllRessionRestoreDevicesWithTransaction:nil]; } } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index f8d1e9a55..9c28757fa 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2672,3 +2672,5 @@ "Your device was unlinked successfully" = "Your device was unlinked successfully"; "Unnamed Device" = "Unnamed Device"; "Linked device (%@)" = "Linked device (%@)"; +"Restore session" = "Restore session"; +"Would you like to start a new session with %@?" = "Would you like to start a new session with %@?"; diff --git a/SignalServiceKit/src/Loki/Messaging/LKSessionRestoreMessage.h b/SignalServiceKit/src/Loki/Messaging/LKSessionRestoreMessage.h new file mode 100644 index 000000000..876cde8d2 --- /dev/null +++ b/SignalServiceKit/src/Loki/Messaging/LKSessionRestoreMessage.h @@ -0,0 +1,8 @@ +#import "LKFriendRequestMessage.h" + +NS_SWIFT_NAME(FriendRequestMessage) +@interface LKSessionRestoreMessage : LKFriendRequestMessage + +- (instancetype)initWithThread:(TSThread *)thread; + +@end diff --git a/SignalServiceKit/src/Loki/Messaging/LKSessionRestoreMessage.m b/SignalServiceKit/src/Loki/Messaging/LKSessionRestoreMessage.m new file mode 100644 index 000000000..43ca9c3ea --- /dev/null +++ b/SignalServiceKit/src/Loki/Messaging/LKSessionRestoreMessage.m @@ -0,0 +1,23 @@ +#import "LKSessionRestoreMessage.h" +#import +#import + +@implementation LKSessionRestoreMessage + +#pragma mark Initialization +- (instancetype)initWithThread:(TSThread *)thread { + return [self initOutgoingMessageWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageBody:@"" attachmentIds:[NSMutableArray new] + expiresInSeconds:0 expireStartedAt:0 isVoiceMessage:NO groupMetaMessage:TSGroupMetaMessageUnspecified quotedMessage:nil contactShare:nil linkPreview:nil]; +} + +#pragma mark Building +- (nullable SSKProtoDataMessageBuilder *)dataMessageBuilder +{ + SSKProtoDataMessageBuilder *builder = super.dataMessageBuilder; + if (builder == nil) { return nil; } + [builder setFlags:SSKProtoDataMessageFlagsSessionRestore]; + return builder; +} + + +@end diff --git a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m index 63e37e645..72f0064a5 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m @@ -89,8 +89,9 @@ NSUInteger TSErrorMessageSchemaVersion = 1; withTransaction:(YapDatabaseReadWriteTransaction *)transaction failedMessageType:(TSErrorMessageType)errorMessageType { + NSString *source = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source; TSContactThread *contactThread = - [TSContactThread getOrCreateThreadWithContactId:envelope.source transaction:transaction]; + [TSContactThread getOrCreateThreadWithContactId:source transaction:transaction]; // Legit usage of senderTimestamp. We don't actually currently surface it in the UI, but it serves as // a reference to the envelope which we failed to process. diff --git a/SignalServiceKit/src/Messages/OWSMessageDecrypter.m b/SignalServiceKit/src/Messages/OWSMessageDecrypter.m index 78cadcd99..cfcede47b 100644 --- a/SignalServiceKit/src/Messages/OWSMessageDecrypter.m +++ b/SignalServiceKit/src/Messages/OWSMessageDecrypter.m @@ -688,15 +688,15 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes envelope:(SSKProtoEnvelope *)envelope transaction:(YapDatabaseReadWriteTransaction *)transaction { - NSString *masterHexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction]; - NSString *hexEncodedPublicKey = masterHexEncodedPublicKey ?: envelope.source; + NSString *hexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source; TSThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction]; // Trigger a session restore prompt if we get specific errors if (errorMessage.errorType == TSErrorMessageNoSession || errorMessage.errorType == TSErrorMessageInvalidMessage || errorMessage.errorType == TSErrorMessageInvalidKeyException) { - [((TSContactThread *) contactThread) addSessionRestoreDevice:hexEncodedPublicKey transaction:transaction]; + // We want to store the source device pubkey into the session restore incase it's a secondary device message + [((TSContactThread *) contactThread) addSessionRestoreDevice:envelope.source transaction:transaction]; } } @@ -704,8 +704,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes envelope:(SSKProtoEnvelope *)envelope transaction:(YapDatabaseReadWriteTransaction *)transaction { - NSString *masterHexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction]; - NSString *hexEncodedPublicKey = masterHexEncodedPublicKey ?: envelope.source; + NSString *hexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source; TSThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction]; [SSKEnvironment.shared.notificationsManager notifyUserForErrorMessage:errorMessage thread:contactThread diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m index 67849777b..ab283db93 100644 --- a/SignalServiceKit/src/Messages/OWSMessageManager.m +++ b/SignalServiceKit/src/Messages/OWSMessageManager.m @@ -1723,10 +1723,6 @@ NS_ASSUME_NONNULL_BEGIN }]; } message.friendRequestStatus = LKMessageFriendRequestStatusPending; // Don't save yet. This is done in finalizeIncomingMessage:thread:masterThread:envelope:transaction. - } else { - // This can happen if Alice and Bob have a session, Bob deletes his app, restores from seed, and then sends a friend request to Alice again. - // TODO: Re-enable when seed restoration is done -// [self handleEndSessionMessageWithEnvelope:envelope dataMessage:data transaction:transaction]; } } diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.h b/SignalServiceKit/src/Messages/OWSMessageSender.h index f5f2e1364..f223aadcf 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.h +++ b/SignalServiceKit/src/Messages/OWSMessageSender.h @@ -97,6 +97,7 @@ NS_SWIFT_NAME(MessageSender) success:(void (^)(void))successHandler failure:(void (^)(NSError *error))failureHandler; +- (OWSMessageSend *)getSessionRestoreMessageForHexEncodedPublicKey:(NSString *)hexEncodedPublicKey; - (OWSMessageSend *)getMultiDeviceFriendRequestMessageForHexEncodedPublicKey:(NSString *)hexEncodedPublicKey; - (void)sendMessage:(OWSMessageSend *)messageSend; diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 307d7c8ea..4d11a2e62 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -45,6 +45,7 @@ #import "TSThread.h" #import "TSContactThread.h" #import "LKFriendRequestMessage.h" +#import "LKSessionRestoreMessage.h" #import "LKDeviceLinkMessage.h" #import "LKAddressMessage.h" #import @@ -915,15 +916,32 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; return deviceMessages; } +- (OWSMessageSend *)getSessionRestoreMessageForHexEncodedPublicKey:(NSString *)hexEncodedPublicKey +{ + __block TSContactThread *thread; + [OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + thread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction]; + // Force hide secondary device thread + NSString *masterHexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:hexEncodedPublicKey in:transaction]; + thread.isForceHidden = masterHexEncodedPublicKey != nil && ![masterHexEncodedPublicKey isEqualToString:hexEncodedPublicKey]; + [thread saveWithTransaction:transaction]; + }]; + if (!thread) { return nil; } + LKSessionRestoreMessage *message = [[LKSessionRestoreMessage alloc] initWithThread:thread]; + message.skipSave = YES; + SignalRecipient *recipient = [[SignalRecipient alloc] initWithUniqueId:hexEncodedPublicKey]; + NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; + return [[OWSMessageSend alloc] initWithMessage:message thread:thread recipient:recipient senderCertificate:nil udAccess:nil localNumber:userHexEncodedPublicKey success:^{ } failure:^(NSError *error) { }]; +} + - (OWSMessageSend *)getMultiDeviceFriendRequestMessageForHexEncodedPublicKey:(NSString *)hexEncodedPublicKey { __block TSContactThread *thread; [OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - thread = [TSContactThread fetchObjectWithUniqueID:[TSContactThread threadIdFromContactId:hexEncodedPublicKey] transaction:transaction]; - if (thread == nil) { - thread = [[TSContactThread alloc] initWithContactId:hexEncodedPublicKey]; - } - thread.isForceHidden = YES; + thread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction]; + // Force hide secondary device thread + NSString *masterHexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:hexEncodedPublicKey in:transaction]; + thread.isForceHidden = masterHexEncodedPublicKey != nil && ![masterHexEncodedPublicKey isEqualToString:hexEncodedPublicKey]; [thread saveWithTransaction:transaction]; }]; LKFriendRequestMessage *message = [[LKFriendRequestMessage alloc] initOutgoingMessageWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageBody:@"Please accept to enable messages to be synced across devices" attachmentIds:[NSMutableArray new]