diff --git a/Scripts/sds_codegen/sds_config/sds-config.json b/Scripts/sds_codegen/sds_config/sds-config.json index 2440c763e0..5d5b31fb94 100644 --- a/Scripts/sds_codegen/sds_config/sds-config.json +++ b/Scripts/sds_codegen/sds_config/sds-config.json @@ -13,7 +13,8 @@ "OWSBackupFragment.uncompressedDataLength": "UInt64", "TestModel.nsNumberValueUsingInt64": "Int64", "TestModel.nsNumberValueUsingUInt64": "UInt64", - "SSKJobRecord.exclusiveProcessIdentifier": "Int32" + "SSKJobRecord.exclusiveProcessIdentifier": "Int32", + "TSMessage.storyTimestamp": "UInt64" }, "properties_to_ignore": [ "TSYapDatabaseObject.grdbId", diff --git a/Scripts/sds_codegen/sds_config/sds-property_order.json b/Scripts/sds_codegen/sds_config/sds-property_order.json index 18980983fe..a563f341cc 100644 --- a/Scripts/sds_codegen/sds_config/sds-property_order.json +++ b/Scripts/sds_codegen/sds_config/sds-property_order.json @@ -83,6 +83,7 @@ "InteractionRecord.hasSyncedTranscript": 23, "InteractionRecord.infoMessageUserInfo": 50, "InteractionRecord.isFromLinkedDevice": 24, + "InteractionRecord.isGroupStoryReply": 67, "InteractionRecord.isLocalChange": 25, "InteractionRecord.isViewOnceComplete": 26, "InteractionRecord.isViewOnceMessage": 27, @@ -113,6 +114,9 @@ "InteractionRecord.sourceDeviceId": 44, "InteractionRecord.storedMessageState": 45, "InteractionRecord.storedShouldStartExpireTimer": 46, + "InteractionRecord.storyAuthorUuidString": 65, + "InteractionRecord.storyReactionEmoji": 68, + "InteractionRecord.storyTimestamp": 66, "InteractionRecord.timestamp": 2, "InteractionRecord.uniqueThreadId": 3, "InteractionRecord.unregisteredAddress": 47, diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index ee2cfd3fbf..a91bb9d161 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -861,6 +861,11 @@ 88594E5A2335B08000390B19 /* AudioMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88594E592335B08000390B19 /* AudioMessageView.swift */; }; 885C35502370DFD50004BA35 /* OWSSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885C354F2370DFD50004BA35 /* OWSSyncManager.swift */; }; 8862A55925F090C5005D65DB /* InternalSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8862A55825F090C5005D65DB /* InternalSettingsViewController.swift */; }; + 8864072827EEA658009916B6 /* StoryGroupReplySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072727EEA658009916B6 /* StoryGroupReplySheet.swift */; }; + 8864072A27F0D426009916B6 /* StoryGroupReplyLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072927F0D426009916B6 /* StoryGroupReplyLoader.swift */; }; + 8864072C27F0DA38009916B6 /* StoryGroupReplyViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */; }; + 8864072E27F0E8DF009916B6 /* StoryGroupReplyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */; }; + 8864073127F21AD7009916B6 /* StoryReplyInputToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */; }; 886A58CA276A760600A1099B /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 886A58C8276A760600A1099B /* SubscriptionManager.swift */; }; 886A58CB276A760600A1099B /* SubscriptionReceiptCredentialRedemptionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 886A58C9276A760600A1099B /* SubscriptionReceiptCredentialRedemptionJob.swift */; }; 886BB3D225BA0C9D00079781 /* PreviewWallpaperViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88ABAB8E25B8BE3F0008C78A /* PreviewWallpaperViewController.swift */; }; @@ -2087,6 +2092,11 @@ 88594E592335B08000390B19 /* AudioMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessageView.swift; sourceTree = ""; }; 885C354F2370DFD50004BA35 /* OWSSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSSyncManager.swift; sourceTree = ""; }; 8862A55825F090C5005D65DB /* InternalSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalSettingsViewController.swift; sourceTree = ""; }; + 8864072727EEA658009916B6 /* StoryGroupReplySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplySheet.swift; sourceTree = ""; }; + 8864072927F0D426009916B6 /* StoryGroupReplyLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyLoader.swift; sourceTree = ""; }; + 8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyViewItem.swift; sourceTree = ""; }; + 8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyCell.swift; sourceTree = ""; }; + 8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryReplyInputToolbar.swift; sourceTree = ""; }; 886A58C8276A760600A1099B /* SubscriptionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = ""; }; 886A58C9276A760600A1099B /* SubscriptionReceiptCredentialRedemptionJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionReceiptCredentialRedemptionJob.swift; sourceTree = ""; }; 886CB07724E77E5B00753909 /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = ""; }; @@ -4058,6 +4068,7 @@ 884DB94427DD70F700C6A309 /* IncomingStoryViewModel.swift */, 884DB94627DD754700C6A309 /* StoryCell.swift */, 885275BF27E26775003F2F9B /* TextAttachmentView.swift */, + 8864072F27F21AA7009916B6 /* Group Reply Sheet */, 884DB94A27DE66E000C6A309 /* Context View */, ); path = Stories; @@ -4135,6 +4146,18 @@ path = Individual; sourceTree = ""; }; + 8864072F27F21AA7009916B6 /* Group Reply Sheet */ = { + isa = PBXGroup; + children = ( + 8864072727EEA658009916B6 /* StoryGroupReplySheet.swift */, + 8864072927F0D426009916B6 /* StoryGroupReplyLoader.swift */, + 8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */, + 8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */, + 8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */, + ); + path = "Group Reply Sheet"; + sourceTree = ""; + }; 8871B16D25F007CE00D4E070 /* Profile */ = { isa = PBXGroup; children = ( @@ -6146,6 +6169,7 @@ 8855DF87238F1E0C0066D96F /* EmojiReactorsTableView.swift in Sources */, 3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */, 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */, + 8864073127F21AD7009916B6 /* StoryReplyInputToolbar.swift in Sources */, 326DF2612739F4D90017B789 /* FeaturedBadgeViewController.swift in Sources */, 34ACA7D92733159600E47AD4 /* OnboardingController.swift in Sources */, 88A9729422FB4D02004B4FBF /* LocationPicker.swift in Sources */, @@ -6174,6 +6198,7 @@ 34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */, 349767E325B8744700ECE1B0 /* ManageStickersViewController.swift in Sources */, 34A8B3512190A40E00218A25 /* CVMediaAlbumView.swift in Sources */, + 8864072A27F0D426009916B6 /* StoryGroupReplyLoader.swift in Sources */, 4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */, 4CB5F26720F6E1E2004D1B42 /* MessageActionsToolbar.swift in Sources */, 3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */, @@ -6235,6 +6260,7 @@ 34ACA7EA2733159600E47AD4 /* ChangePhoneNumberCaptchaViewController.swift in Sources */, 8806EF19248DBD7200E764C7 /* NotificationPermissionReminderMegaphone.swift in Sources */, 34404271273AFBBE008C9FA4 /* ChangePhoneNumber2FAViewController.swift in Sources */, + 8864072C27F0DA38009916B6 /* StoryGroupReplyViewItem.swift in Sources */, 343A65951FC47D5E000477A1 /* DebugUISyncMessages.m in Sources */, 88D23D1223CEBFB200B0E74B /* NotificationActionHandler.swift in Sources */, 880C2E02262A19DE006650B6 /* InteractiveSheetViewController.swift in Sources */, @@ -6278,6 +6304,7 @@ 3444E6BB264EDFF300B32E3B /* CVColorOrGradientView.swift in Sources */, 327CF66825ACE7DD00DA0A6F /* GetStartedBannerViewController.swift in Sources */, 34ACA7DF2733159600E47AD4 /* Onboarding2FAViewController.swift in Sources */, + 8864072E27F0E8DF009916B6 /* StoryGroupReplyCell.swift in Sources */, 8862A55925F090C5005D65DB /* InternalSettingsViewController.swift in Sources */, 88A941992409A391000E9700 /* LottieToggleButton.swift in Sources */, 342FFE7A271EF581000AC89F /* UIResponder+OWS.swift in Sources */, @@ -6289,6 +6316,7 @@ 348BB25D20A0C5530047AEC2 /* ContactShareViewHelper.swift in Sources */, 3497971525D6D55400E99FA4 /* PaymentsSendRecipientViewController.swift in Sources */, 884DB94F27DE67BB00C6A309 /* StoryPageViewController.swift in Sources */, + 8864072827EEA658009916B6 /* StoryGroupReplySheet.swift in Sources */, 88A4CC1B246CEC8B0082211F /* DeviceTransferQRScanningViewController.swift in Sources */, 34E95C24269F4F4F004807EC /* CLVViewState.swift in Sources */, 34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */, diff --git a/Signal/Images.xcassets/add-reaction-outline-24.imageset/Contents.json b/Signal/Images.xcassets/add-reaction-outline-24.imageset/Contents.json new file mode 100644 index 0000000000..5a929fd630 --- /dev/null +++ b/Signal/Images.xcassets/add-reaction-outline-24.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "add-reaction-outline-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Images.xcassets/add-reaction-outline-24.imageset/add-reaction-outline-24.pdf b/Signal/Images.xcassets/add-reaction-outline-24.imageset/add-reaction-outline-24.pdf new file mode 100644 index 0000000000..b41e82b2b7 Binary files /dev/null and b/Signal/Images.xcassets/add-reaction-outline-24.imageset/add-reaction-outline-24.pdf differ diff --git a/Signal/Images.xcassets/messages-solid-20.imageset/Contents.json b/Signal/Images.xcassets/messages-solid-20.imageset/Contents.json new file mode 100644 index 0000000000..1952fd6f3f --- /dev/null +++ b/Signal/Images.xcassets/messages-solid-20.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "messages-solid-20.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Images.xcassets/messages-solid-20.imageset/messages-solid-20.pdf b/Signal/Images.xcassets/messages-solid-20.imageset/messages-solid-20.pdf new file mode 100644 index 0000000000..19e22122a1 Binary files /dev/null and b/Signal/Images.xcassets/messages-solid-20.imageset/messages-solid-20.pdf differ diff --git a/Signal/Images.xcassets/reply-solid-20.imageset/Contents.json b/Signal/Images.xcassets/reply-solid-20.imageset/Contents.json new file mode 100644 index 0000000000..34de88ae1a --- /dev/null +++ b/Signal/Images.xcassets/reply-solid-20.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "reply-solid-20.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Images.xcassets/reply-solid-20.imageset/reply-solid-20.pdf b/Signal/Images.xcassets/reply-solid-20.imageset/reply-solid-20.pdf new file mode 100644 index 0000000000..11ae2c3e27 Binary files /dev/null and b/Signal/Images.xcassets/reply-solid-20.imageset/reply-solid-20.pdf differ diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVLoader.swift b/Signal/src/ViewControllers/ConversationView/CV/CVLoader.swift index 0c24431563..7fe0d9cc5c 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVLoader.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVLoader.swift @@ -161,7 +161,9 @@ public class CVLoader: NSObject { self.benchSteps.step("messageMapping") let thread = threadViewModel.threadRecord - let threadInteractionCount = thread.numberOfInteractions(with: transaction) + let threadInteractionCount = thread.numberOfInteractions( + with: messageMapping.storyReplyQueryMode, + transaction: transaction) self.benchSteps.step("threadInteractionCount") diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVMessageMapping.swift b/Signal/src/ViewControllers/ConversationView/CV/CVMessageMapping.swift index f1f5662f05..93a7f912a3 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVMessageMapping.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVMessageMapping.swift @@ -3,6 +3,7 @@ // import Foundation +import SignalServiceKit public class CVMessageMapping: NSObject { @@ -18,11 +19,15 @@ public class CVMessageMapping: NSObject { public var canLoadNewer = false - private let thread: TSThread + public let storyReplyQueryMode: StoryReplyQueryMode - public required init(thread: TSThread) { - self.thread = thread - self.interactionFinder = InteractionFinder(threadUniqueId: thread.uniqueId) + public convenience init(thread: TSThread, storyReplyQueryMode: StoryReplyQueryMode = .excludeGroupReplies) { + self.init(threadUniqueId: thread.uniqueId, storyReplyQueryMode: storyReplyQueryMode) + } + + public required init(threadUniqueId: String, storyReplyQueryMode: StoryReplyQueryMode) { + self.interactionFinder = InteractionFinder(threadUniqueId: threadUniqueId) + self.storyReplyQueryMode = storyReplyQueryMode } // The smaller this number is, the faster the conversation can display. @@ -167,7 +172,10 @@ public class CVMessageMapping: NSObject { let count = max(1, min(count, maxInteractionCount)) // The number of visible interactions currently in the conversation. - let conversationSize = interactionFinder.count(excludingPlaceholders: !DebugFlags.showFailedDecryptionPlaceholders.get(), transaction: transaction) + let conversationSize = interactionFinder.count( + excludingPlaceholders: !DebugFlags.showFailedDecryptionPlaceholders.get(), + storyReplyQueryMode: storyReplyQueryMode, + transaction: transaction) guard conversationSize > 0 else { self.loadedInteractions = [] updateCanLoadMore(fetchIndexSet: IndexSet(), conversationSize: conversationSize) @@ -196,6 +204,7 @@ public class CVMessageMapping: NSObject { guard let distanceFromLatest = try self.interactionFinder.distanceFromLatest( interactionUniqueId: interactionUniqueId, excludingPlaceholders: !DebugFlags.showFailedDecryptionPlaceholders.get(), + storyReplyQueryMode: self.storyReplyQueryMode, transaction: transaction) else { throw OWSAssertionError("viewIndex was unexpectedly nil") } @@ -481,6 +490,7 @@ public class CVMessageMapping: NSObject { var newItems: [TSInteraction] = [] try self.interactionFinder.enumerateInteractions(range: nsRange, excludingPlaceholders: !DebugFlags.showFailedDecryptionPlaceholders.get(), + storyReplyQueryMode: self.storyReplyQueryMode, transaction: transaction) { (interaction: TSInteraction, _) in newItems.append(interaction) } @@ -493,6 +503,7 @@ public class CVMessageMapping: NSObject { let interactionIds = try interactionFinder.interactionIds( inRange: nsRange, excludingPlaceholders: !DebugFlags.showFailedDecryptionPlaceholders.get(), + storyReplyQueryMode: storyReplyQueryMode, transaction: transaction) guard !interactionIds.isEmpty else { return [] @@ -572,7 +583,9 @@ public class CVMessageMapping: NSObject { var oldestUnreadInteraction: TSInteraction? private func updateOldestUnreadInteraction(transaction: SDSAnyReadTransaction) throws { - self.oldestUnreadInteraction = try interactionFinder.oldestUnreadInteraction(transaction: transaction.unwrapGrdbRead) + self.oldestUnreadInteraction = try interactionFinder.oldestUnreadInteraction( + storyReplyQueryMode: storyReplyQueryMode, + transaction: transaction.unwrapGrdbRead) } public var debugInteractions: String { diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.m b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.m index 233db64133..f2f9b5576c 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.m +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.m @@ -720,8 +720,8 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup"; self.isViewVisible = YES; - // Ensure the tabBar is always hidden if stories is disabled. - BOOL shouldHideTabBar = !SSKFeatureFlags.stories; + // Ensure the tabBar is always hidden if stories is disabled or we're in the archive. + BOOL shouldHideTabBar = !SSKFeatureFlags.stories || self.chatListMode == ChatListModeArchive; if (shouldHideTabBar) { self.tabBarController.tabBar.hidden = YES; self.extendedLayoutIncludesOpaqueBars = YES; diff --git a/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryContextViewController.swift b/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryContextViewController.swift index db26594bcd..64e15a7bcd 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryContextViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryContextViewController.swift @@ -6,6 +6,7 @@ import Foundation import SignalServiceKit import UIKit import SignalUI +import BonMot protocol StoryContextViewControllerDelegate: AnyObject { func storyContextViewControllerWantsTransitionToNextContext( @@ -30,16 +31,7 @@ class StoryContextViewController: OWSViewController { private var items = [StoryItem]() var currentItem: StoryItem? { didSet { - currentItemMediaView?.removeFromSuperview() - - if let currentItem = currentItem { - let itemView = StoryItemMediaView(item: currentItem) - self.currentItemMediaView = itemView - mediaViewContainer.addSubview(itemView) - itemView.autoPinEdgesToSuperviewEdges() - } - - updateProgressState() + currentItemWasUpdated(messageDidChange: oldValue?.message.uniqueId != currentItem?.message.uniqueId) } } var currentItemMediaView: StoryItemMediaView? @@ -121,6 +113,7 @@ class StoryContextViewController: OWSViewController { private lazy var closeButton = OWSButton(imageName: "x-24", tintColor: .ows_white) private lazy var mediaViewContainer = UIView() + private lazy var replyButton = OWSFlatButton() override func viewDidLoad() { super.viewDidLoad() @@ -138,10 +131,44 @@ class StoryContextViewController: OWSViewController { view.addSubview(mediaViewContainer) + replyButton.setPressedBlock { [weak self] in + guard let self = self, let currentItem = self.currentItem else { return } + switch self.context { + case .groupId: + let groupReplyVC = StoryGroupReplySheet(storyMessage: currentItem.message) + groupReplyVC.dismissHandler = { [weak self] in self?.play() } + self.pause() + self.present(groupReplyVC, animated: true) + case .authorUuid: + // todo: 1:1 replies + break + case .none: + owsFailDebug("Unexpected context") + } + } + replyButton.setBackgroundColors(upColor: .clear) + replyButton.autoSetDimension(.height, toSize: 64) + replyButton.setTitleColor(Theme.darkThemePrimaryColor) + view.addSubview(replyButton) + replyButton.autoPinEdge(.leading, to: .leading, of: mediaViewContainer) + replyButton.autoPinEdge(.trailing, to: .trailing, of: mediaViewContainer) + + view.addSubview(playbackProgressView) + playbackProgressView.autoPinEdge(.leading, to: .leading, of: mediaViewContainer, withOffset: OWSTableViewController2.defaultHOuterMargin) + playbackProgressView.autoPinEdge(.trailing, to: .trailing, of: mediaViewContainer, withOffset: -OWSTableViewController2.defaultHOuterMargin) + playbackProgressView.autoSetDimension(.height, toSize: 2) + playbackProgressView.isUserInteractionEnabled = false + if UIDevice.current.hasIPhoneXNotch || UIDevice.current.isIPad { + // iPhone with notch or iPad (views/replies rendered below media, media is in a card) mediaViewContainer.layer.cornerRadius = 18 mediaViewContainer.clipsToBounds = true + replyButton.autoPinEdge(.top, to: .bottom, of: mediaViewContainer) + playbackProgressView.autoPinEdge(.bottom, to: .top, of: replyButton, withOffset: -OWSTableViewController2.defaultHOuterMargin) } else { + // iPhone with home button (views/replies rendered on top of media, media is fullscreen) + replyButton.autoPinEdge(.bottom, to: .bottom, of: mediaViewContainer) + playbackProgressView.autoPinEdge(.bottom, to: .top, of: replyButton) mediaViewContainer.autoPinEdge(toSuperviewSafeArea: .bottom) } @@ -162,13 +189,6 @@ class StoryContextViewController: OWSViewController { closeButton.autoPinEdge(toSuperviewSafeArea: .top) closeButton.autoPinEdge(toSuperviewSafeArea: .leading) - view.addSubview(playbackProgressView) - playbackProgressView.autoPinEdge(.leading, to: .leading, of: mediaViewContainer, withOffset: OWSTableViewController2.defaultHOuterMargin) - playbackProgressView.autoPinEdge(.trailing, to: .trailing, of: mediaViewContainer, withOffset: -OWSTableViewController2.defaultHOuterMargin) - playbackProgressView.autoPinEdge(.bottom, to: .bottom, of: mediaViewContainer, withOffset: -OWSTableViewController2.defaultHOuterMargin) - playbackProgressView.autoSetDimension(.height, toSize: 2) - playbackProgressView.isUserInteractionEnabled = false - loadStoryItems { [weak self] storyItems in // If there are no stories for this context, dismiss. guard !storyItems.isEmpty else { @@ -206,6 +226,8 @@ class StoryContextViewController: OWSViewController { } private func buildStoryItem(for message: StoryMessage, transaction: SDSAnyReadTransaction) -> StoryItem? { + let replyCount = InteractionFinder.countReplies(for: message, transaction: transaction) + switch message.attachment { case .file(let attachmentId): guard let attachment = TSAttachment.anyFetch(uniqueId: attachmentId, transaction: transaction) else { @@ -213,18 +235,53 @@ class StoryContextViewController: OWSViewController { return nil } if let attachment = attachment as? TSAttachmentPointer { - return .init(message: message, attachment: .pointer(attachment)) + return .init(message: message, numberOfReplies: replyCount, attachment: .pointer(attachment)) } else if let attachment = attachment as? TSAttachmentStream { - return .init(message: message, attachment: .stream(attachment)) + return .init(message: message, numberOfReplies: replyCount, attachment: .stream(attachment)) } else { owsFailDebug("Unexpected attachment type \(type(of: attachment))") return nil } case .text(let attachment): - return .init(message: message, attachment: .text(attachment)) + return .init(message: message, numberOfReplies: replyCount, attachment: .text(attachment)) } } + private func currentItemWasUpdated(messageDidChange: Bool) { + currentItemMediaView?.removeFromSuperview() + + if let currentItem = currentItem { + let itemView = StoryItemMediaView(item: currentItem) + self.currentItemMediaView = itemView + mediaViewContainer.addSubview(itemView) + itemView.autoPinEdgesToSuperviewEdges() + + replyButton.isHidden = false + + let replyButtonText: String + switch currentItem.numberOfReplies { + case 0: + replyButtonText = NSLocalizedString("STORY_REPLY_BUTTON_WITH_NO_REPLIES", comment: "Button for replying to a story with no existing replies.") + case 1: + replyButtonText = NSLocalizedString("STORY_REPLY_BUTTON_WITH_1_REPLY", comment: "Button for replying to a story with a single existing reply.") + default: + let format = NSLocalizedString("STORY_REPLY_BUTTON_WITH_N_REPLIES_FORMAT", comment: "Button for replying to a story with N existing replies. {embeds the number of replies}") + replyButtonText = String(format: format, "\(currentItem.numberOfReplies)") + } + + let semiboldStyle = StringStyle(.font(.systemFont(ofSize: 17, weight: .semibold))) + let attributedReplyButtonText = replyButtonText.styled( + with: .font(.systemFont(ofSize: 17, weight: .regular)), + .xmlRules([.style("bold", semiboldStyle)]) + ) + replyButton.setAttributedTitle(attributedReplyButtonText) + } else { + replyButton.isHidden = true + } + + if messageDidChange { updateProgressState() } + } + private var pauseTime: CFTimeInterval? private var lastTransitionTime: CFTimeInterval? private static let transitionDuration: CFTimeInterval = 5 @@ -363,28 +420,39 @@ extension StoryContextViewController: UIGestureRecognizerDelegate { func handleLongPress() { switch pauseGestureRecognizer.state { case .began: - pauseTime = CACurrentMediaTime() - delegate?.storyContextViewControllerDidPause(self) - currentItemMediaView?.pause { - self.playbackProgressView.alpha = 0 - self.closeButton.alpha = 0 - } + pause(hideChrome: true) case .ended: - if let lastTransitionTime = lastTransitionTime, let pauseTime = pauseTime { - let pauseDuration = CACurrentMediaTime() - pauseTime - self.lastTransitionTime = lastTransitionTime + pauseDuration - self.pauseTime = nil - } - currentItemMediaView?.play { - self.playbackProgressView.alpha = 1 - self.closeButton.alpha = 1 - } - delegate?.storyContextViewControllerDidResume(self) + play() default: break } } + func pause(hideChrome: Bool = false) { + guard pauseTime == nil else { return } + pauseTime = CACurrentMediaTime() + delegate?.storyContextViewControllerDidPause(self) + currentItemMediaView?.pause(hideChrome: hideChrome) { + if hideChrome { + self.playbackProgressView.alpha = 0 + self.closeButton.alpha = 0 + } + } + } + + func play() { + if let lastTransitionTime = lastTransitionTime, let pauseTime = pauseTime { + let pauseDuration = CACurrentMediaTime() - pauseTime + self.lastTransitionTime = lastTransitionTime + pauseDuration + self.pauseTime = nil + } + currentItemMediaView?.play { + self.playbackProgressView.alpha = 1 + self.closeButton.alpha = 1 + } + delegate?.storyContextViewControllerDidResume(self) + } + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { let touchLocation = gestureRecognizer.location(in: view) if gestureRecognizer == leftTapGestureRecognizer { @@ -436,7 +504,7 @@ extension StoryContextViewController: DatabaseChangeDelegate { } DispatchQueue.main.async { if shouldDismiss { - self.dismiss(animated: true) + self.presentingViewController?.dismiss(animated: true) } else { self.items = newItems self.currentItem = currentItem diff --git a/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryItemMediaView.swift b/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryItemMediaView.swift index 7671bb45d8..28ec15fdb4 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryItemMediaView.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryItemMediaView.swift @@ -45,15 +45,17 @@ class StoryItemMediaView: UIView { gradientProtectionView.alpha = 1 } - func pause(animateAlongside: @escaping () -> Void) { + func pause(hideChrome: Bool = false, animateAlongside: @escaping () -> Void) { videoPlayer?.pause() - UIView.animate(withDuration: 0.15, delay: 0, options: [.beginFromCurrentState, .curveEaseInOut]) { - self.authorRow.alpha = 0 - self.gradientProtectionView.alpha = 0 + if hideChrome { + UIView.animate(withDuration: 0.15, delay: 0, options: [.beginFromCurrentState, .curveEaseInOut]) { + self.authorRow.alpha = 0 + self.gradientProtectionView.alpha = 0 + animateAlongside() + } completion: { _ in } + } else { animateAlongside() - } completion: { _ in - } } @@ -157,7 +159,14 @@ class StoryItemMediaView: UIView { addSubview(authorRow) authorRow.autoPinWidthToSuperview(withMargin: OWSTableViewController2.defaultHOuterMargin) - authorRow.autoPinEdge(toSuperviewEdge: .bottom, withInset: OWSTableViewController2.defaultHOuterMargin + 16) + + if UIDevice.current.hasIPhoneXNotch || UIDevice.current.isIPad { + // iPhone with notch or iPad (views/replies rendered below media, media is in a card) + authorRow.autoPinEdge(toSuperviewEdge: .bottom, withInset: OWSTableViewController2.defaultHOuterMargin + 16) + } else { + // iPhone with home button (views/replies rendered on top of media, media is fullscreen) + authorRow.autoPinEdge(toSuperviewEdge: .bottom, withInset: 80) + } } private func buildAvatarView(transaction: SDSAnyReadTransaction) -> UIView { @@ -406,6 +415,7 @@ class StoryItemMediaView: UIView { class StoryItem: NSObject { let message: StoryMessage + let numberOfReplies: UInt enum Attachment { case pointer(TSAttachmentPointer) case stream(TSAttachmentStream) @@ -413,8 +423,9 @@ class StoryItem: NSObject { } var attachment: Attachment - init(message: StoryMessage, attachment: Attachment) { + init(message: StoryMessage, numberOfReplies: UInt, attachment: Attachment) { self.message = message + self.numberOfReplies = numberOfReplies self.attachment = attachment } } diff --git a/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryPlaybackProgressView.swift b/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryPlaybackProgressView.swift index edb775f2e0..cb2ad7851b 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryPlaybackProgressView.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryPlaybackProgressView.swift @@ -3,6 +3,7 @@ // import Foundation +import UIKit class StoryPlaybackProgressView: UIView { var playedColor: UIColor = .ows_white { @@ -102,32 +103,59 @@ class StoryPlaybackProgressView: UIView { unplayedShapeLayer.path = unplayedBezierPath.cgPath } - for x in 0.. 0 { + for idx in 0.. 0 { + var playedXPosition = CGFloat(idx) * (itemWidth + itemSpacing) + let playedWidth = itemWidth * itemState.value + let playedRoundedCorners: UIRectCorner + var unplayedXPosition = playedXPosition + playedWidth + let unplayedRoundedCorners: UIRectCorner + + if CurrentAppContext().isRTL { + unplayedXPosition = rect.width - playedXPosition - itemWidth + playedXPosition = unplayedXPosition + itemWidth - playedWidth + playedRoundedCorners = [.topRight, .bottomRight] + unplayedRoundedCorners = [.topLeft, .bottomLeft] + } else { + playedRoundedCorners = [.topLeft, .bottomLeft] + unplayedRoundedCorners = [.topRight, .bottomRight] + } + let playedItemFrame = CGRect( - x: CGFloat(x) * (itemWidth + itemSpacing), + x: playedXPosition, y: 0, - width: itemWidth * itemState.value, + width: playedWidth, height: itemHeight ) - playedBezierPath.append(UIBezierPath(roundedRect: playedItemFrame, byRoundingCorners: [.topLeft, .bottomLeft], cornerRadii: CGSize(square: itemHeight / 2))) + playedBezierPath.append(UIBezierPath( + roundedRect: playedItemFrame, + byRoundingCorners: playedRoundedCorners, + cornerRadii: CGSize(square: itemHeight / 2))) + let unplayedItemFrame = CGRect( - x: playedItemFrame.x + playedItemFrame.width, + x: unplayedXPosition, y: 0, width: itemWidth * (1 - itemState.value), height: itemHeight ) - unplayedBezierPath.append(UIBezierPath(roundedRect: unplayedItemFrame, byRoundingCorners: [.topRight, .bottomRight], cornerRadii: CGSize(square: itemHeight / 2))) + unplayedBezierPath.append(UIBezierPath( + roundedRect: unplayedItemFrame, + byRoundingCorners: unplayedRoundedCorners, + cornerRadii: CGSize(square: itemHeight / 2))) } else { let path: UIBezierPath - if itemState.index < x || (itemState.index == x && itemState.value <= 0) { + if itemState.index < idx || (itemState.index == idx && itemState.value <= 0) { path = unplayedBezierPath } else { - owsAssertDebug(itemState.index > x || (itemState.index == x && itemState.value >= 1)) + owsAssertDebug(itemState.index > idx || (itemState.index == idx && itemState.value >= 1)) path = playedBezierPath } + + var xPosition = CGFloat(idx) * (itemWidth + itemSpacing) + if CurrentAppContext().isRTL { xPosition = rect.width - xPosition - itemWidth } + let itemFrame = CGRect( - x: CGFloat(x) * (itemWidth + itemSpacing), + x: xPosition, y: 0, width: itemWidth, height: itemHeight diff --git a/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryGroupReplyCell.swift b/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryGroupReplyCell.swift new file mode 100644 index 0000000000..a5f3839469 --- /dev/null +++ b/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryGroupReplyCell.swift @@ -0,0 +1,337 @@ +// +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. +// + +import Foundation +import UIKit +import SignalMessaging + +class StoryGroupReplyCell: UITableViewCell { + lazy var avatarView = ConversationAvatarView(sizeClass: .twentyEight, localUserDisplayMode: .asUser, useAutolayout: true) + lazy var messageLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + return label + }() + lazy var authorNameLabel: UILabel = { + let label = UILabel() + label.font = UIFont.ows_dynamicTypeFootnoteClamped.ows_semibold + return label + }() + lazy var reactionLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 28) + label.textAlignment = .trailing + return label + }() + lazy var bubbleView: UIView = { + let view = UIView() + view.backgroundColor = .ows_gray80 + view.layoutMargins = UIEdgeInsets(hMargin: 12, vMargin: 7) + return view + }() + lazy var bubbleCornerMaskLayer = CAShapeLayer() + + enum CellType: String, CaseIterable { + case standalone + case top + case bottom + case middle + case reaction + + var hasTimestamp: Bool { + switch self { + case .standalone, .bottom, .reaction: return true + case .top, .middle: return false + } + } + + var hasAuthor: Bool { + switch self { + case .standalone, .top, .reaction: return true + case .middle, .bottom: return false + } + } + + var hasAvatar: Bool { + switch self { + case .standalone, .bottom, .reaction: return true + case .top, .middle: return false + } + } + + var hasBubble: Bool { + switch self { + case .reaction: return false + case .standalone, .top, .middle, .bottom: return true + } + } + + var hasReaction: Bool { + switch self { + case .reaction: return true + case .standalone, .top, .middle, .bottom: return false + } + } + + var insets: UIEdgeInsets { + switch self { + case .standalone, .reaction: return UIEdgeInsets(hMargin: 16, vMargin: 6) + case .top: return UIEdgeInsets(top: 6, leading: 16, bottom: 1, trailing: 16) + case .middle: return UIEdgeInsets(top: 1, leading: 16, bottom: 1, trailing: 16) + case .bottom: return UIEdgeInsets(top: 1, leading: 16, bottom: 6, trailing: 16) + } + } + + var verticalAlignment: UIStackView.Alignment { + switch self { + case .reaction: return .center + case .standalone, .top, .middle, .bottom: return .bottom + } + } + + var sharpCorners: UIRectCorner { + switch self { + case .standalone, .reaction: return [] + case .top: return CurrentAppContext().isRTL ? .bottomRight : .bottomLeft + case .middle: return CurrentAppContext().isRTL ? [.bottomRight, .topRight] : [.bottomLeft, .topLeft] + case .bottom: return CurrentAppContext().isRTL ? .topRight : .topLeft + } + } + + var cornerRadius: CGFloat { 18 } + var sharpCornerRadius: CGFloat { 4 } + } + let cellType: CellType + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + if let reuseIdentifier = reuseIdentifier, let cellType = CellType(rawValue: reuseIdentifier) { + self.cellType = cellType + } else { + owsFailDebug("Missing cellType for reuseIdentifer \(String(describing: reuseIdentifier))") + self.cellType = .standalone + } + + super.init(style: style, reuseIdentifier: reuseIdentifier) + + selectionStyle = .none + backgroundColor = .clear + + let vStack = UIStackView() + vStack.axis = .vertical + vStack.spacing = 2 + + let hStack = UIStackView() + hStack.axis = .horizontal + hStack.alignment = cellType.verticalAlignment + + hStack.isLayoutMarginsRelativeArrangement = true + hStack.layoutMargins = cellType.insets + + if cellType.hasAvatar { + avatarView.autoSetDimensions(to: CGSize(square: 28)) + hStack.addArrangedSubview(avatarView) + } else { + hStack.addArrangedSubview(.spacer(withWidth: 28)) + } + + hStack.addArrangedSubview(.spacer(withWidth: 8)) + + if cellType.hasBubble { + hStack.addArrangedSubview(bubbleView) + bubbleView.addSubview(vStack) + vStack.autoPinEdgesToSuperviewMargins() + + if cellType.sharpCorners.isEmpty { + bubbleView.layer.cornerRadius = cellType.cornerRadius + bubbleView.clipsToBounds = true + } else { + bubbleView.layer.mask = bubbleCornerMaskLayer + } + } else { + hStack.addArrangedSubview(.spacer(withWidth: 12)) + hStack.addArrangedSubview(vStack) + } + + hStack.addArrangedSubview(.hStretchingSpacer()) + + if cellType.hasReaction { + hStack.addArrangedSubview(reactionLabel) + } + + if cellType.hasAuthor { + vStack.addArrangedSubview(authorNameLabel) + } + + vStack.addArrangedSubview(messageLabel) + + contentView.addSubview(hStack) + hStack.autoPinEdgesToSuperviewEdges() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(with item: StoryGroupReplyViewItem) { + if cellType.hasAuthor { + authorNameLabel.textColor = item.authorColor + authorNameLabel.text = item.authorDisplayName + } + + if cellType.hasAvatar { + avatarView.updateWithSneakyTransactionIfNecessary { config in + config.dataSource = .address(item.authorAddress) + } + } + + if cellType.hasReaction { + reactionLabel.text = item.reactionEmoji + } + + configureTextAndTimestamp(for: item) + } + + func configureTextAndTimestamp(for item: StoryGroupReplyViewItem) { + guard let messageText: NSAttributedString = { + if item.wasRemotelyDeleted { + return NSLocalizedString("THIS_MESSAGE_WAS_DELETED", comment: "text indicating the message was remotely deleted").styled( + with: .font(UIFont.ows_dynamicTypeBodyClamped.ows_italic), + .color(.ows_gray05) + ) + } else if cellType == .reaction { + return NSLocalizedString("STORY_REPLY_REACTION", comment: "Text indicating a story has been reacted to").styled( + with: .font(.ows_dynamicTypeBodyClamped), + .color(.ows_gray05), + .alignment(.natural) + ) + } else if let displayableText = item.displayableText { + return displayableText.displayAttributedText.styled( + with: .font(.ows_dynamicTypeBodyClamped), + .color(.ows_gray05), + .alignment(displayableText.displayTextNaturalAlignment) + ) + } else { + return nil + } + }() else { return } + + guard cellType.hasTimestamp else { + messageLabel.attributedText = messageText + return + } + + // Append timestamp to attributed text + let timestampText = item.timeString.styled( + with: .font(.ows_dynamicTypeCaption1Clamped), + .color(.ows_gray25) + ) + + let maxMessageWidth = min(512, CurrentAppContext().frame.width) - 92 + let timestampSpacer: CGFloat = 6 + + let messageMeasurement = measure(messageText, maxWidth: maxMessageWidth) + let timestampMeasurement = measure(timestampText, maxWidth: maxMessageWidth) + + let lastLineFreeSpace = maxMessageWidth - timestampSpacer - messageMeasurement.lastLineRect.width + + let textDirectionMatchesAppDirection: Bool + switch item.displayableText?.displayTextNaturalAlignment ?? .natural { + case .left: + textDirectionMatchesAppDirection = !CurrentAppContext().isRTL + case .right: + textDirectionMatchesAppDirection = CurrentAppContext().isRTL + case .natural: + textDirectionMatchesAppDirection = true + default: + owsFailDebug("Unexpected text alignment") + textDirectionMatchesAppDirection = true + } + + let hasSpacedForTimestampOnLastMessageLine = lastLineFreeSpace >= timestampMeasurement.rect.width + let shouldRenderTimestampOnLastMessageLine = hasSpacedForTimestampOnLastMessageLine && textDirectionMatchesAppDirection + + if shouldRenderTimestampOnLastMessageLine { + var possibleMessageBubbleWidths = [ + messageMeasurement.rect.width, + messageMeasurement.lastLineRect.width + timestampSpacer + timestampMeasurement.rect.width + ] + if cellType.hasAuthor { + contentView.layoutIfNeeded() + possibleMessageBubbleWidths.append(authorNameLabel.width) + } + + let finalMessageLabelWidth = possibleMessageBubbleWidths.max()! + + messageLabel.attributedText = .composed(of: [ + messageText, + "\n", + timestampText.styled( + with: .paragraphSpacingBefore(-timestampMeasurement.rect.height), + .firstLineHeadIndent(finalMessageLabelWidth - timestampMeasurement.rect.width) + ) + ]) + } else { + var possibleMessageBubbleWidths = [ + messageMeasurement.rect.width, + timestampMeasurement.rect.width + ] + if cellType.hasAuthor { + contentView.layoutIfNeeded() + possibleMessageBubbleWidths.append(authorNameLabel.width) + } + + let finalMessageLabelWidth = possibleMessageBubbleWidths.max()! + + messageLabel.attributedText = .composed(of: [ + messageText, + "\n", + timestampText.styled( + with: textDirectionMatchesAppDirection + ? .firstLineHeadIndent(finalMessageLabelWidth - timestampMeasurement.rect.width) + : .alignment(.trailing) + ) + ]) + } + } + + private func measure(_ attributedString: NSAttributedString, maxWidth: CGFloat) -> (rect: CGRect, lastLineRect: CGRect) { + guard !attributedString.isEmpty else { return (.zero, .zero) } + + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: CGSize(width: maxWidth, height: .infinity)) + let textStorage = NSTextStorage() + + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + textStorage.setAttributedString(attributedString) + + textContainer.lineFragmentPadding = 0 + textContainer.lineBreakMode = .byWordWrapping + textContainer.maximumNumberOfLines = 0 + + let lastGlyphIndex = layoutManager.glyphIndexForCharacter(at: attributedString.length - 1) + let lastLineFragmentRect = layoutManager.lineFragmentUsedRect( + forGlyphAt: lastGlyphIndex, + effectiveRange: nil + ) + + let fullTextRect = layoutManager.usedRect(for: textContainer) + + return (fullTextRect, lastLineFragmentRect) + } + + override func layoutSubviews() { + super.layoutSubviews() + + guard !cellType.sharpCorners.isEmpty else { return } + + bubbleView.layoutIfNeeded() + bubbleCornerMaskLayer.path = UIBezierPath.roundedRect( + bubbleView.bounds, + sharpCorners: cellType.sharpCorners, + sharpCornerRadius: cellType.sharpCornerRadius, + wideCornerRadius: cellType.cornerRadius + ).cgPath + } +} diff --git a/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryGroupReplyLoader.swift b/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryGroupReplyLoader.swift new file mode 100644 index 0000000000..e1157cee0b --- /dev/null +++ b/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryGroupReplyLoader.swift @@ -0,0 +1,304 @@ +// +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. +// + +import Foundation +import SignalCoreKit + +class StoryGroupReplyLoader: Dependencies { + private let loadingLock = UnfairLock() + private var messageMapping: CVMessageMapping! + private let threadUniqueId: String + private let storyMessage: StoryMessage + private weak var tableView: UITableView? + private var replyUniqueIds = [String]() { + didSet { AssertIsOnMainThread() } + } + private var replyItems = [String: StoryGroupReplyViewItem]() { + didSet { AssertIsOnMainThread() } + } + + var numberOfRows: Int { replyUniqueIds.count } + + var oldestLoadedRow: Int? { + let loadedInteractions = loadingLock.withLock { messageMapping.loadedInteractions } + guard let uniqueId = loadedInteractions.first?.uniqueId else { return nil } + return replyUniqueIds.firstIndex(of: uniqueId) + } + + var newestLoadedRow: Int? { + let loadedInteractions = loadingLock.withLock { messageMapping.loadedInteractions } + guard let uniqueId = loadedInteractions.last?.uniqueId else { return nil } + return replyUniqueIds.firstIndex(of: uniqueId) + } + + var isScrolledToBottom: Bool { + AssertIsOnMainThread() + return tableView?.indexPathsForVisibleRows?.lazy.map { $0.row }.contains(replyUniqueIds.count - 1) ?? false + } + + init?(storyMessage: StoryMessage, threadUniqueId: String?, tableView: UITableView) { + guard let threadUniqueId = threadUniqueId else { + owsFailDebug("Unexpectedly missing threadUniqueId") + return nil + } + + self.threadUniqueId = threadUniqueId + self.storyMessage = storyMessage + self.tableView = tableView + + // Load the first page synchronously. + databaseStorage.read { transaction in + messageMapping = CVMessageMapping( + threadUniqueId: threadUniqueId, + storyReplyQueryMode: .onlyGroupReplies(storyTimestamp: storyMessage.timestamp) + ) + + load(mode: .initial, transaction: transaction) + } + + databaseStorage.appendDatabaseChangeDelegate(self) + } + + func replyItem(for indexPath: IndexPath) -> StoryGroupReplyViewItem? { + guard let uniqueId = replyUniqueIds[safe: indexPath.row] else { + owsFailDebug("Unexpectedly missing uniqueId for indexPath \(indexPath)") + return nil + } + + return replyItems[uniqueId] + } + + func scrollToBottomOfLoadWindow(animated: Bool) { + guard numberOfRows > 0 else { return } + tableView?.scrollToRow(at: IndexPath(row: numberOfRows - 1, section: 0), at: .bottom, animated: animated) + } + + func loadNewerPageIfNecessary() { + LoadingMode.newer.async { + guard self.loadingLock.withLock({ self.messageMapping.canLoadNewer }) else { return } + self.databaseStorage.read { self.load(mode: .newer, transaction: $0) } + } + } + + func loadOlderPageIfNecessary() { + LoadingMode.older.async { + guard self.loadingLock.withLock({ self.messageMapping.canLoadOlder }) else { return } + self.databaseStorage.read { self.load(mode: .older, transaction: $0) } + } + } + + func reload( + updatedInteractionIds: Set? = nil, + deletedInteractionIds: Set? = nil, + canReuseInteractions: Bool = true + ) { + LoadingMode.reload.async { + self.databaseStorage.read { self.load( + mode: .reload, + canReuseInteractions: canReuseInteractions, + updatedInteractionIds: updatedInteractionIds, + deletedInteractionIds: deletedInteractionIds, + transaction: $0 + ) } + } + } + + private enum LoadingMode: Equatable { + case initial + case older + case newer + case reload + + func async(block: @escaping () -> Void) { + switch self { + case .initial: + DispatchMainThreadSafe(block) + default: + DispatchQueue.sharedUserInteractive.async(execute: block) + } + } + + var queue: DispatchQueue { + switch self { + case .initial: return .main + default: return .sharedUserInteractive + } + } + } + + private func load( + mode: LoadingMode, + canReuseInteractions: Bool = true, + updatedInteractionIds: Set? = nil, + deletedInteractionIds: Set? = nil, + transaction: SDSAnyReadTransaction + ) { + assertOnQueue(mode.queue) + + Logger.info("Loading \(mode)") + + let reusableInteractions: [String: TSInteraction] + if canReuseInteractions { + let loadedInteractions = loadingLock.withLock { messageMapping.loadedInteractions } + reusableInteractions = loadedInteractions.reduce( + into: [String: TSInteraction]() + ) { partialResult, interaction in + guard updatedInteractionIds?.contains(interaction.uniqueId) != true else { return } + partialResult[interaction.uniqueId] = interaction + } + } else { + reusableInteractions = [:] + } + + loadingLock.withLock { + do { + switch mode { + case .initial: + try self.messageMapping.loadNewestMessagePage( + reusableInteractions: reusableInteractions, + deletedInteractionIds: nil, + transaction: transaction + ) + case .newer: + try self.messageMapping.loadNewerMessagePage( + reusableInteractions: reusableInteractions, + deletedInteractionIds: nil, + transaction: transaction + ) + case .older: + try self.messageMapping.loadOlderMessagePage( + reusableInteractions: reusableInteractions, + deletedInteractionIds: nil, + transaction: transaction + ) + case .reload: + try self.messageMapping.loadSameLocation( + reusableInteractions: reusableInteractions, + deletedInteractionIds: nil, + transaction: transaction + ) + } + } catch { + owsFailDebug("Load failed for mode \(mode): \(error)") + return + } + } + + let newReplyItems = buildItems(reusableInteractionIds: Array(reusableInteractions.keys), transaction: transaction) + let replyUniqueIds = InteractionFinder.groupReplyUniqueIds(for: self.storyMessage, transaction: transaction) + + DispatchQueue.main.async { + let wasScrolledToBottom = self.isScrolledToBottom + let wasInitialLoad = mode == .initial + + self.replyUniqueIds = replyUniqueIds + self.replyItems = newReplyItems + self.tableView?.reloadData() + + if wasScrolledToBottom || wasInitialLoad { self.scrollToBottomOfLoadWindow(animated: !wasInitialLoad) } + } + } + + private func buildItems(reusableInteractionIds: [String], transaction: SDSAnyReadTransaction) -> [String: StoryGroupReplyViewItem] { + guard let groupThread = TSGroupThread.anyFetchGroupThread(uniqueId: threadUniqueId, transaction: transaction) else { + owsFailDebug("Missing group thread for story") + return replyItems + } + + let loadedInteractions = loadingLock.withLock { messageMapping.loadedInteractions } + + var messages = [(SignalServiceAddress, TSMessage)]() + var authorAddresses = Set() + + for interaction in loadedInteractions { + if let outgoingMessage = interaction as? TSOutgoingMessage { + messages.append((tsAccountManager.localAddress!, outgoingMessage)) + authorAddresses.insert(tsAccountManager.localAddress!) + } else if let incomingMessage = interaction as? TSIncomingMessage { + messages.append((incomingMessage.authorAddress, incomingMessage)) + authorAddresses.insert(incomingMessage.authorAddress) + } + } + + let groupNameColors = ChatColors.groupNameColors(forThread: groupThread) + let displayNamesByAddress = contactsManagerImpl.displayNamesByAddress( + for: Array(authorAddresses), + transaction: transaction + ) + + var newReplyItems = [String: StoryGroupReplyViewItem]() + var previousItem: StoryGroupReplyViewItem? + for (authorAddress, message) in messages { + let replyItem: StoryGroupReplyViewItem + if reusableInteractionIds.contains(message.uniqueId), let reusableReplyItem = replyItems[message.uniqueId] { + replyItem = reusableReplyItem + } else { + let displayName = authorAddress.isLocalAddress + ? NSLocalizedString( + "STORY_REPLY_YOU_AUTHOR", + comment: "Author text when the local user author's a story reply" + ) + : displayNamesByAddress[authorAddress] + replyItem = StoryGroupReplyViewItem( + message: message, + authorAddress: authorAddress, + authorDisplayName: displayName, + authorColor: groupNameColors.color(for: authorAddress), + transaction: transaction + ) + } + + newReplyItems[message.uniqueId] = replyItem + + if let previousItem = previousItem { + if previousItem.authorAddress == authorAddress + && previousItem.timeString == replyItem.timeString + && replyItem.cellType != .reaction + && previousItem.cellType != .reaction { + switch previousItem.cellType { + case .standalone: + previousItem.cellType = .top + case .bottom: + previousItem.cellType = .middle + case .top, .middle, .reaction: + break + } + + replyItem.cellType = .bottom + } else { + switch previousItem.cellType { + case .standalone, .reaction, .bottom: + break + case .top: + previousItem.cellType = .standalone + case .middle: + previousItem.cellType = .bottom + } + } + } + + previousItem = replyItem + } + + return newReplyItems + } +} + +extension StoryGroupReplyLoader: DatabaseChangeDelegate { + func databaseChangesDidUpdate(databaseChanges: DatabaseChanges) { + guard let storyId = storyMessage.id, databaseChanges.storyMessageRowIds.contains(storyId) else { return } + reload( + updatedInteractionIds: databaseChanges.interactionUniqueIds, + deletedInteractionIds: databaseChanges.interactionDeletedUniqueIds + ) + } + + func databaseChangesDidUpdateExternally() { + reload(canReuseInteractions: false) + } + + func databaseChangesDidReset() { + reload(canReuseInteractions: false) + } +} diff --git a/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryGroupReplySheet.swift b/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryGroupReplySheet.swift new file mode 100644 index 0000000000..e36b04408f --- /dev/null +++ b/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryGroupReplySheet.swift @@ -0,0 +1,360 @@ +// +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. +// + +import Foundation +import UIKit +import SignalServiceKit + +class StoryGroupReplySheet: InteractiveSheetViewController { + override var renderExternalHandle: Bool { false } + + private lazy var tableView = UITableView() + private lazy var inputToolbar = StoryReplyInputToolbar() + private lazy var inputToolbarBottomConstraint = inputToolbar.autoPinEdge(toSuperviewEdge: .bottom) + + private lazy var inputAccessoryPlaceholder: InputAccessoryViewPlaceholder = { + let placeholder = InputAccessoryViewPlaceholder() + placeholder.delegate = self + placeholder.referenceView = view + return placeholder + }() + + private let storyMessage: StoryMessage + private let thread: TSThread? + + fileprivate var reactionPickerBackdrop: UIView? + fileprivate var reactionPicker: MessageReactionPicker? + + var dismissHandler: (() -> Void)? + + init(storyMessage: StoryMessage) { + self.storyMessage = storyMessage + self.thread = Self.databaseStorage.read { transaction in + if let groupId = storyMessage.groupId { + return TSGroupThread.fetch(groupId: groupId, transaction: transaction) + } else { + owsFailDebug("Unexpectedly received non-group thread for story reply sheet.") + return TSContactThread.getWithContactAddress(storyMessage.authorAddress, transaction: transaction) + } + } + } + + public required init() { + fatalError("init() has not been implemented") + } + + fileprivate var replyLoader: StoryGroupReplyLoader? + override func viewDidLoad() { + super.viewDidLoad() + + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + tableView.rowHeight = UITableView.automaticDimension + tableView.keyboardDismissMode = .interactive + tableView.contentInset = UIEdgeInsets(top: 30, left: 0, bottom: 0, right: 0) + + contentView.addSubview(tableView) + tableView.autoPinEdgesToSuperviewEdges() + + // We add the handle directly to the content view, + // so that it doesn't scroll with the table. + let handleContainer = UIView() + contentView.addSubview(handleContainer) + handleContainer.autoPinWidthToSuperview() + handleContainer.autoPinEdge(toSuperviewEdge: .top) + + let handle = UIView() + handle.backgroundColor = .ows_gray65 + handle.autoSetDimensions(to: CGSize(width: 36, height: 5)) + handle.layer.cornerRadius = 5 / 2 + handleContainer.addSubview(handle) + handle.autoPinHeightToSuperview(withMargin: 12) + handle.autoHCenterInSuperview() + + inputToolbar.delegate = self + contentView.addSubview(inputToolbar) + inputToolbar.autoPinWidthToSuperview() + inputToolbarBottomConstraint.isActive = true + + contentView.backgroundColor = .ows_gray90 + tableView.backgroundColor = .ows_gray90 + handleContainer.backgroundColor = .ows_gray90 + + for type in StoryGroupReplyCell.CellType.allCases { + tableView.register(StoryGroupReplyCell.self, forCellReuseIdentifier: type.rawValue) + } + + replyLoader = StoryGroupReplyLoader(storyMessage: storyMessage, threadUniqueId: thread?.uniqueId, tableView: tableView) + } + + public override var canBecomeFirstResponder: Bool { true } + + public override var inputAccessoryView: UIView? { inputAccessoryPlaceholder } + + override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + super.dismiss(animated: flag) { [dismissHandler] in + completion?() + dismissHandler?() + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + replyLoader?.scrollToBottomOfLoadWindow(animated: true) + } + + fileprivate func tryToSendMessage(_ message: TSOutgoingMessage) { + guard let thread = thread else { + return owsFailDebug("Unexpectedly missing thread") + } + + guard !blockingManager.isThreadBlocked(thread) else { + BlockListUIUtils.showUnblockThreadActionSheet(thread, from: self) { [weak self] isBlocked in + guard !isBlocked else { return } + self?.tryToSendMessage(message) + } + return + } + + guard !SafetyNumberConfirmationSheet.presentIfNecessary( + addresses: thread.recipientAddresses, + confirmationText: SafetyNumberStrings.confirmSendButton, + completion: { [weak self] didConfirmIdentity in + guard didConfirmIdentity else { return } + self?.tryToSendMessage(message) + } + ) else { return } + + ThreadUtil.enqueueSendAsyncWrite { [weak self] transaction in + ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequest(thread: thread, setDefaultTimerIfNecessary: false, transaction: transaction) + + message.anyInsert(transaction: transaction) + + Self.messageSenderJobQueue.add(message: message.asPreparer, transaction: transaction) + + transaction.addAsyncCompletionOnMain { + self?.replyLoader?.reload() + self?.inputToolbar.messageBody = nil + } + } + } + + func tryToSendReaction(_ reaction: String) { + owsAssertDebug(reaction.isSingleEmoji) + + guard let thread = thread else { + return owsFailDebug("Unexpectedly missing thread") + } + + let builder = TSOutgoingMessageBuilder(thread: thread) + builder.storyReactionEmoji = reaction + builder.storyTimestamp = NSNumber(value: storyMessage.timestamp) + builder.storyAuthorAddress = storyMessage.authorAddress + + tryToSendMessage(builder.build()) + } +} + +extension StoryGroupReplySheet: UIScrollViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard let visibleRows = tableView.indexPathsForVisibleRows?.map({ $0.row }), + !visibleRows.isEmpty, + let oldestLoadedRow = replyLoader?.oldestLoadedRow, + let newestLoadedRow = replyLoader?.newestLoadedRow else { return } + + let rowsFromTop = (visibleRows.min() ?? oldestLoadedRow) - oldestLoadedRow + let rowsFromBottom = newestLoadedRow - (visibleRows.max() ?? newestLoadedRow) + + if rowsFromTop <= 30 { + replyLoader?.loadOlderPageIfNecessary() + } + + if rowsFromBottom <= 30 { + replyLoader?.loadNewerPageIfNecessary() + } + } +} + +extension StoryGroupReplySheet: UITableViewDelegate { + +} + +extension StoryGroupReplySheet: UITableViewDataSource { + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let item = replyLoader?.replyItem(for: indexPath) else { + owsFailDebug("Missing item for cell at indexPath \(indexPath)") + return UITableViewCell() + } + + let cell = tableView.dequeueReusableCell(withIdentifier: item.cellType.rawValue, for: indexPath) as! StoryGroupReplyCell + cell.configure(with: item) + + return cell + } + + func numberOfSections(in tableView: UITableView) -> Int { + 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + replyLoader?.numberOfRows ?? 0 + } +} + +extension StoryGroupReplySheet: InputAccessoryViewPlaceholderDelegate { + public func inputAccessoryPlaceholderKeyboardIsPresenting(animationDuration: TimeInterval, animationCurve: UIView.AnimationCurve) { + handleKeyboardStateChange(animationDuration: animationDuration, animationCurve: animationCurve) + } + + public func inputAccessoryPlaceholderKeyboardDidPresent() { + updateInputToolbarPosition() + updateContentInsets(animated: false) + } + + public func inputAccessoryPlaceholderKeyboardIsDismissing(animationDuration: TimeInterval, animationCurve: UIView.AnimationCurve) { + handleKeyboardStateChange(animationDuration: animationDuration, animationCurve: animationCurve) + } + + public func inputAccessoryPlaceholderKeyboardDidDismiss() { + updateInputToolbarPosition() + updateContentInsets(animated: false) + } + + public func inputAccessoryPlaceholderKeyboardIsDismissingInteractively() { + updateInputToolbarPosition() + } + + func handleKeyboardStateChange(animationDuration: TimeInterval, animationCurve: UIView.AnimationCurve) { + guard animationDuration > 0 else { + updateInputToolbarPosition() + updateContentInsets(animated: false) + return + } + + UIView.beginAnimations("keyboardStateChange", context: nil) + UIView.setAnimationBeginsFromCurrentState(true) + UIView.setAnimationCurve(animationCurve) + UIView.setAnimationDuration(animationDuration) + updateInputToolbarPosition() + updateContentInsets(animated: true) + UIView.commitAnimations() + } + + func updateInputToolbarPosition() { + inputToolbarBottomConstraint.constant = -inputAccessoryPlaceholder.keyboardOverlap + + // We always want to apply the new bottom bar position immediately, + // as this only happens during animations (interactive or otherwise) + inputToolbar.superview?.layoutIfNeeded() + } + + func updateContentInsets(animated: Bool) { + let wasScrolledToBottom = replyLoader?.isScrolledToBottom ?? false + tableView.contentInset.bottom = inputAccessoryPlaceholder.keyboardOverlap + inputToolbar.height - view.safeAreaInsets.bottom + if wasScrolledToBottom { + replyLoader?.scrollToBottomOfLoadWindow(animated: animated) + } + } +} + +extension StoryGroupReplySheet: StoryReplyInputToolbarDelegate { + func storyReplyInputToolbarDidTapReact(_ storyReplyInputToolbar: StoryReplyInputToolbar) { + presentReactionPicker() + } + + func storyReplyInputToolbarDidTapSend(_ storyReplyInputToolbar: StoryReplyInputToolbar) { + guard let messageBody = storyReplyInputToolbar.messageBody, !messageBody.text.isEmpty else { + return owsFailDebug("Unexpectedly missing message body") + } + + guard let thread = thread else { + return owsFailDebug("Unexpectedly missing thread") + } + + let builder = TSOutgoingMessageBuilder(thread: thread) + builder.messageBody = messageBody.text + builder.bodyRanges = messageBody.ranges + builder.storyTimestamp = NSNumber(value: storyMessage.timestamp) + builder.storyAuthorAddress = storyMessage.authorAddress + + tryToSendMessage(builder.build()) + } + + func storyReplyInputToolbarDidBeginEditing(_ storyReplyInputToolbar: StoryReplyInputToolbar) { + maximizeHeight() + } + + func storyReplyInputToolbarHeightDidChange(_ storyReplyInputToolbar: StoryReplyInputToolbar) { + updateContentInsets(animated: false) + } + + func storyReplyInputToolbarMentionPickerPossibleAddresses(_ storyReplyInputToolbar: StoryReplyInputToolbar) -> [SignalServiceAddress] { + return thread?.recipientAddresses ?? [] + } +} + +extension StoryGroupReplySheet: MessageReactionPickerDelegate { + func didSelectReaction(reaction: String, isRemoving: Bool, inPosition position: Int) { + dismissReactionPicker() + + tryToSendReaction(reaction) + } + + func didSelectAnyEmoji() { + dismissReactionPicker() + + let sheet = EmojiPickerSheet { [weak self] selectedEmoji in + guard let selectedEmoji = selectedEmoji else { return } + self?.tryToSendReaction(selectedEmoji.rawValue) + } + present(sheet, animated: true) + } + + @objc + func didTapReactionPickerBackdrop() { + dismissReactionPicker() + } + + func presentReactionPicker() { + guard self.reactionPicker == nil else { return } + + let backdrop = UIView() + backdrop.backgroundColor = .ows_blackAlpha40 + view.addSubview(backdrop) + backdrop.autoPinEdgesToSuperviewEdges() + backdrop.alpha = 0 + self.reactionPickerBackdrop = backdrop + + let backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapReactionPickerBackdrop)) + backdrop.addGestureRecognizer(backdropTapGesture) + backdrop.isUserInteractionEnabled = true + + let reactionPicker = MessageReactionPicker(selectedEmoji: nil, delegate: self, forceDarkTheme: true) + + view.addSubview(reactionPicker) + reactionPicker.autoPinEdge(.bottom, to: .top, of: inputToolbar, withOffset: -15) + reactionPicker.autoPinEdge(toSuperviewEdge: .trailing, withInset: 12) + + reactionPicker.playPresentationAnimation(duration: 0.2) + + UIView.animate(withDuration: 0.2) { backdrop.alpha = 1 } + + self.reactionPicker = reactionPicker + } + + func dismissReactionPicker() { + UIView.animate(withDuration: 0.2) { + self.reactionPickerBackdrop?.alpha = 0 + } completion: { _ in + self.reactionPickerBackdrop?.removeFromSuperview() + self.reactionPickerBackdrop = nil + } + + reactionPicker?.playDismissalAnimation(duration: 0.2) { + self.reactionPicker?.removeFromSuperview() + self.reactionPicker = nil + } + } +} diff --git a/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryGroupReplyViewItem.swift b/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryGroupReplyViewItem.swift new file mode 100644 index 0000000000..835ed144e8 --- /dev/null +++ b/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryGroupReplyViewItem.swift @@ -0,0 +1,53 @@ +// +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. +// + +import Foundation +import SignalServiceKit +import UIKit + +class StoryGroupReplyViewItem: Dependencies { + let displayableText: DisplayableText? + let reactionEmoji: String? + let wasRemotelyDeleted: Bool + let receivedAtTimestamp: UInt64 + let authorDisplayName: String? + let authorAddress: SignalServiceAddress + let authorColor: UIColor + + var cellType: StoryGroupReplyCell.CellType + + var timeString: String { DateUtil.formatMessageTimestampForCVC(receivedAtTimestamp, shouldUseLongFormat: false) } + + init( + message: TSMessage, + authorAddress: SignalServiceAddress, + authorDisplayName: String?, + authorColor: UIColor, + transaction: SDSAnyReadTransaction + ) { + if !message.wasRemotelyDeleted { + self.displayableText = DisplayableText.displayableText( + withMessageBody: .init(text: message.body ?? "", ranges: message.bodyRanges ?? .empty), + mentionStyle: .groupReply, + transaction: transaction + ) + } else { + self.displayableText = nil + } + + self.wasRemotelyDeleted = message.wasRemotelyDeleted + self.receivedAtTimestamp = message.receivedAtTimestamp + self.authorAddress = authorAddress + self.authorDisplayName = authorDisplayName + self.authorColor = authorColor + + if let reactionEmoji = message.storyReactionEmoji { + self.cellType = .reaction + self.reactionEmoji = reactionEmoji + } else { + self.cellType = .standalone + self.reactionEmoji = nil + } + } +} diff --git a/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryReplyInputToolbar.swift b/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryReplyInputToolbar.swift new file mode 100644 index 0000000000..a35b6ade96 --- /dev/null +++ b/Signal/src/ViewControllers/HomeView/Stories/Group Reply Sheet/StoryReplyInputToolbar.swift @@ -0,0 +1,302 @@ +// +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. +// + +import Foundation +import UIKit +import SignalUI + +// Coincides with Android's max text message length +let kMaxMessageBodyCharacterCount = 2000 + +protocol StoryReplyInputToolbarDelegate: AnyObject { + func storyReplyInputToolbarDidTapSend(_ storyReplyInputToolbar: StoryReplyInputToolbar) + func storyReplyInputToolbarDidTapReact(_ storyReplyInputToolbar: StoryReplyInputToolbar) + func storyReplyInputToolbarDidBeginEditing(_ storyReplyInputToolbar: StoryReplyInputToolbar) + func storyReplyInputToolbarHeightDidChange(_ storyReplyInputToolbar: StoryReplyInputToolbar) + func storyReplyInputToolbarMentionPickerPossibleAddresses(_ storyReplyInputToolbar: StoryReplyInputToolbar) -> [SignalServiceAddress] +} + +// MARK: - + +class StoryReplyInputToolbar: UIView { + + weak var delegate: StoryReplyInputToolbarDelegate? + + var messageBody: MessageBody? { + get { textView.messageBody } + set { + textView.messageBody = newValue + updateContent() + } + } + + override var bounds: CGRect { + didSet { + guard oldValue.height != bounds.height else { return } + delegate?.storyReplyInputToolbarHeightDidChange(self) + } + } + + let minTextViewHeight: CGFloat = 36 + var maxTextViewHeight: CGFloat { + // About ~4 lines in portrait and ~3 lines in landscape. + // Otherwise we risk obscuring too much of the content. + return UIDevice.current.orientation.isPortrait ? 160 : 100 + } + var textViewHeightConstraint: NSLayoutConstraint? + + // MARK: - Initializers + + init() { + super.init(frame: CGRect.zero) + + // When presenting or dismissing the keyboard, there may be a slight + // gap between the keyboard and the bottom of the input bar during + // the animation. Extend the background below the toolbar's bounds + // by this much to mask that extra space. + let backgroundExtension: CGFloat = 500 + + if UIAccessibility.isReduceTransparencyEnabled { + self.backgroundColor = .ows_black + + let extendedBackground = UIView() + addSubview(extendedBackground) + extendedBackground.autoPinWidthToSuperview() + extendedBackground.autoPinEdge(.top, to: .bottom, of: self) + extendedBackground.autoSetDimension(.height, toSize: backgroundExtension) + } else { + self.backgroundColor = .clear + + let blurEffectView = UIVisualEffectView(effect: Theme.darkThemeBarBlurEffect) + blurEffectView.layer.zPosition = -1 + addSubview(blurEffectView) + blurEffectView.autoPinWidthToSuperview() + blurEffectView.autoPinEdge(toSuperviewEdge: .top) + blurEffectView.autoPinEdge(toSuperviewEdge: .bottom, withInset: -backgroundExtension) + } + + textView.mentionDelegate = self + + let sendButton = OWSButton.sendButton(imageName: "send-solid-24") { [weak self] in + self?.didTapSend() + } + sendButtonContainer.addSubview(sendButton) + + let reactButton = OWSButton(imageName: "add-reaction-outline-24", tintColor: Theme.darkThemePrimaryColor) { [weak self] in + self?.didTapReact() + } + reactButton.autoSetDimensions(to: CGSize(square: 40)) + reactButtonContainer.addSubview(reactButton) + + for button in [sendButton, reactButton] { + button.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) + button.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) + NSLayoutConstraint.autoSetPriority(.defaultLow) { + button.autoPinEdge(toSuperviewEdge: .top) + } + + button.setContentHuggingHigh() + button.setCompressionResistanceHigh() + } + + // The input toolbar should *always* be laid out left-to-right, even when using + // a right-to-left language. The convention for messaging apps is for the send + // button to always be to the right of the input field, even in RTL layouts. + // This means, in most places you'll want to pin deliberately to left/right + // instead of leading/trailing. You'll also want to the semanticContentAttribute + // to ensure horizontal stack views layout left-to-right. + + let hStackView = UIStackView(arrangedSubviews: [ textContainer, sendButtonContainer, reactButtonContainer ]) + hStackView.isLayoutMarginsRelativeArrangement = true + hStackView.layoutMargins = UIEdgeInsets(margin: 12) + hStackView.axis = .horizontal + hStackView.alignment = .bottom + hStackView.spacing = 12 + hStackView.semanticContentAttribute = .forceLeftToRight + + addSubview(hStackView) + hStackView.autoPinEdgesToSuperviewEdges() + + textViewHeightConstraint = textView.autoSetDimension(.height, toSize: minTextViewHeight) + + textContainer.autoPinEdge(toSuperviewMargin: .top) + textContainer.autoPinEdge(toSuperviewMargin: .bottom) + + updateContent() + } + + required init?(coder aDecoder: NSCoder) { + notImplemented() + } + + // MARK: - UIView Overrides + + override var intrinsicContentSize: CGSize { + get { + // Since we have `self.autoresizingMask = UIViewAutoresizingFlexibleHeight`, we must specify + // an intrinsicContentSize. Specifying CGSize.zero causes the height to be determined by autolayout. + return CGSize.zero + } + } + + // MARK: - Subviews + + private lazy var sendButtonContainer = UIView() + private lazy var reactButtonContainer = UIView() + + lazy var textView: MentionTextView = { + let textView = buildTextView() + + textView.scrollIndicatorInsets = UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 3) + textView.mentionDelegate = self + + return textView + }() + + private let placeholderText = OWSLocalizedString( + "STORY_REPLY_TEXT_FIELD_PLACEHOLDER", + comment: "placeholder text for replying to a story" + ) + + private lazy var placeholderTextView: UITextView = { + let placeholderTextView = buildTextView() + + placeholderTextView.text = placeholderText + placeholderTextView.isEditable = false + placeholderTextView.textContainer.maximumNumberOfLines = 1 + placeholderTextView.textContainer.lineBreakMode = .byTruncatingTail + placeholderTextView.textColor = .ows_whiteAlpha60 + + return placeholderTextView + }() + + private lazy var textContainer: UIView = { + let textContainer = UIView() + let textBubble = UIView() + textContainer.addSubview(textBubble) + let inset = (40 - minTextViewHeight) / 2 + textBubble.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 0, leading: 0, bottom: inset, trailing: 0)) + + textBubble.layer.cornerRadius = minTextViewHeight / 2 + textBubble.clipsToBounds = true + textBubble.backgroundColor = .ows_gray75 + + textBubble.addSubview(placeholderTextView) + placeholderTextView.autoPinEdgesToSuperviewEdges() + + textBubble.addSubview(textView) + textView.autoPinEdgesToSuperviewEdges() + + return textContainer + }() + + private func buildTextView() -> MentionTextView { + let textView = MentionTextView() + + textView.keyboardAppearance = Theme.darkThemeKeyboardAppearance + textView.backgroundColor = .clear + textView.tintColor = Theme.darkThemePrimaryColor + + let textViewFont = UIFont.ows_dynamicTypeBody + textView.font = textViewFont + textView.textColor = Theme.darkThemePrimaryColor + + // Check the system font size and increase text inset accordingly + // to keep the text vertically centered + textView.updateVerticalInsetsForDynamicBodyType(defaultInsets: 7) + textView.textContainerInset.left = 7 + textView.textContainerInset.right = 7 + + return textView + } + + // MARK: - Actions + + func didTapSend() { + textView.acceptAutocorrectSuggestion() + delegate?.storyReplyInputToolbarDidTapSend(self) + } + + func didTapReact() { + delegate?.storyReplyInputToolbarDidTapReact(self) + } + + // MARK: - Helpers + + private func updateContent() { + AssertIsOnMainThread() + + updateHeight(textView: textView) + + let isTextViewEmpty = textView.text.isEmptyOrNil + + reactButtonContainer.isHidden = !isTextViewEmpty + sendButtonContainer.isHidden = isTextViewEmpty + placeholderTextView.isHidden = !isTextViewEmpty + } + + private func updateHeight(textView: UITextView) { + guard let textViewHeightConstraint = textViewHeightConstraint else { + owsFailDebug("Missing constraint.") + return + } + + // compute new height assuming width is unchanged + let currentSize = textView.frame.size + let textViewHeight = clampedTextViewHeight(fixedWidth: currentSize.width) + + if textViewHeightConstraint.constant != textViewHeight { + Logger.debug("TextView height changed: \(textViewHeightConstraint.constant) -> \(textViewHeight)") + textViewHeightConstraint.constant = textViewHeight + invalidateIntrinsicContentSize() + } + } + + private func clampedTextViewHeight(fixedWidth: CGFloat) -> CGFloat { + let contentSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude)) + return CGFloatClamp(contentSize.height, minTextViewHeight, maxTextViewHeight) + } +} + +extension StoryReplyInputToolbar: MentionTextViewDelegate { + func textViewDidBeginTypingMention(_ textView: MentionTextView) {} + + func textViewDidEndTypingMention(_ textView: MentionTextView) {} + + func textViewMentionPickerParentView(_ textView: MentionTextView) -> UIView? { + return superview + } + + func textViewMentionPickerReferenceView(_ textView: MentionTextView) -> UIView? { + return self + } + + func textViewMentionPickerPossibleAddresses(_ textView: MentionTextView) -> [SignalServiceAddress] { + return delegate?.storyReplyInputToolbarMentionPickerPossibleAddresses(self) ?? [] + } + + func textView(_ textView: MentionTextView, didDeleteMention mention: Mention) {} + + func textView(_ textView: MentionTextView, shouldResolveMentionForAddress address: SignalServiceAddress) -> Bool { + return textViewMentionPickerPossibleAddresses(textView).contains(address) + } + + func textViewMentionStyle(_ textView: MentionTextView) -> Mention.Style { + return .groupReply + } + + public func textViewDidChange(_ textView: UITextView) { + updateHeight(textView: textView) + updateContent() + } + + public func textViewDidBeginEditing(_ textView: UITextView) { + delegate?.storyReplyInputToolbarDidBeginEditing(self) + updateContent() + } + + public func textViewDidEndEditing(_ textView: UITextView) { + updateContent() + } +} diff --git a/Signal/src/ViewControllers/HomeView/Stories/IncomingStoryViewModel.swift b/Signal/src/ViewControllers/HomeView/Stories/IncomingStoryViewModel.swift index 6e0764f310..64bde68282 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/IncomingStoryViewModel.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/IncomingStoryViewModel.swift @@ -16,7 +16,7 @@ struct IncomingStoryViewModel: Dependencies { case missing } let latestMessageAttachment: Attachment - let latestMessageHasReplies: Bool + let hasReplies: Bool let latestMessageName: String let latestMessageTimestamp: UInt64 @@ -40,6 +40,7 @@ struct IncomingStoryViewModel: Dependencies { } self.context = latestMessage.context + self.hasReplies = InteractionFinder.hasReplies(for: latestMessage.context, transaction: transaction) if let groupId = latestMessage.groupId { guard let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: transaction) else { @@ -74,7 +75,6 @@ struct IncomingStoryViewModel: Dependencies { latestMessageAttachment = .text(attachment) } - latestMessageHasReplies = false // TODO: replies latestMessageTimestamp = latestMessage.timestamp } diff --git a/Signal/src/ViewControllers/HomeView/Stories/StoriesViewController.swift b/Signal/src/ViewControllers/HomeView/Stories/StoriesViewController.swift index a3690eae62..5b4611de55 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/StoriesViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/StoriesViewController.swift @@ -20,12 +20,6 @@ class StoriesViewController: OWSViewController { title = NSLocalizedString("STORIES_TITLE", comment: "Title for the stories view.") - let cameraButton = UIBarButtonItem(image: Theme.iconImage(.cameraButton), style: .plain, target: self, action: #selector(showCameraView)) - cameraButton.accessibilityLabel = NSLocalizedString("CAMERA_BUTTON_LABEL", comment: "Accessibility label for camera button.") - cameraButton.accessibilityHint = NSLocalizedString("CAMERA_BUTTON_HINT", comment: "Accessibility hint describing what you can do with the camera button") - - navigationItem.rightBarButtonItems = [cameraButton] - databaseStorage.appendDatabaseChangeDelegate(self) tableView.register(StoryCell.self, forCellReuseIdentifier: StoryCell.reuseIdentifier) @@ -34,6 +28,7 @@ class StoriesViewController: OWSViewController { tableView.estimatedRowHeight = 116 reloadStories() + updateNavigationBar() } private var timestampUpdateTimer: Timer? @@ -97,6 +92,16 @@ class StoriesViewController: OWSViewController { view.backgroundColor = Theme.secondaryBackgroundColor tableView.backgroundColor = Theme.secondaryBackgroundColor } + + updateNavigationBar() + } + + private func updateNavigationBar() { + let cameraButton = UIBarButtonItem(image: Theme.iconImage(.cameraButton), style: .plain, target: self, action: #selector(showCameraView)) + cameraButton.accessibilityLabel = NSLocalizedString("CAMERA_BUTTON_LABEL", comment: "Accessibility label for camera button.") + cameraButton.accessibilityHint = NSLocalizedString("CAMERA_BUTTON_HINT", comment: "Accessibility hint describing what you can do with the camera button") + + navigationItem.rightBarButtonItems = [cameraButton] } @objc diff --git a/Signal/src/ViewControllers/HomeView/Stories/StoryCell.swift b/Signal/src/ViewControllers/HomeView/Stories/StoryCell.swift index e4f8c73180..4ccfd05e35 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/StoryCell.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/StoryCell.swift @@ -13,14 +13,19 @@ class StoryCell: UITableViewCell { let timestampLabel = UILabel() let avatarView = ConversationAvatarView(sizeClass: .fiftySix, localUserDisplayMode: .asUser, useAutolayout: true) let attachmentThumbnail = UIView() + let replyImageView = UIImageView() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) backgroundColor = .clear - let vStack = UIStackView(arrangedSubviews: [nameLabel, timestampLabel]) + replyImageView.autoSetDimensions(to: CGSize(square: 20)) + replyImageView.contentMode = .scaleAspectFit + + let vStack = UIStackView(arrangedSubviews: [nameLabel, timestampLabel, replyImageView]) vStack.axis = .vertical + vStack.alignment = .leading let hStack = UIStackView(arrangedSubviews: [avatarView, vStack, .hStretchingSpacer(), attachmentThumbnail]) hStack.axis = .horizontal @@ -42,6 +47,18 @@ class StoryCell: UITableViewCell { func configure(with model: IncomingStoryViewModel) { configureTimestamp(with: model) + switch model.context { + case .authorUuid: + replyImageView.image = #imageLiteral(resourceName: "reply-solid-20").withRenderingMode(.alwaysTemplate) + case .groupId: + replyImageView.image = #imageLiteral(resourceName: "messages-solid-20").withRenderingMode(.alwaysTemplate) + case .none: + owsFailDebug("Unexpected context") + } + + replyImageView.isHidden = !model.hasReplies + replyImageView.tintColor = Theme.secondaryTextAndIconColor + nameLabel.font = .ows_dynamicTypeHeadline nameLabel.textColor = Theme.primaryTextColor nameLabel.text = model.latestMessageName diff --git a/Signal/src/ViewControllers/MessageReactionPicker.swift b/Signal/src/ViewControllers/MessageReactionPicker.swift index 76bdef1795..91f1ddd95e 100644 --- a/Signal/src/ViewControllers/MessageReactionPicker.swift +++ b/Signal/src/ViewControllers/MessageReactionPicker.swift @@ -24,7 +24,7 @@ class MessageReactionPicker: UIStackView { private var buttonForEmoji = [(emoji: String, button: OWSFlatButton)]() private var selectedEmoji: EmojiWithSkinTones? private var backgroundView: UIView? - init(selectedEmoji: String?, delegate: MessageReactionPickerDelegate?, configureMode: Bool = false) { + init(selectedEmoji: String?, delegate: MessageReactionPickerDelegate?, configureMode: Bool = false, forceDarkTheme: Bool = false) { self.configureMode = configureMode if let selectedEmoji = selectedEmoji { @@ -38,7 +38,7 @@ class MessageReactionPicker: UIStackView { super.init(frame: .zero) backgroundView = addBackgroundView( - withBackgroundColor: Theme.actionSheetBackgroundColor, + withBackgroundColor: forceDarkTheme ? .ows_gray75 : Theme.actionSheetBackgroundColor, cornerRadius: pickerDiameter / 2 ) backgroundView?.layer.shadowColor = UIColor.ows_black.cgColor @@ -47,7 +47,7 @@ class MessageReactionPicker: UIStackView { backgroundView?.layer.shadowOffset = .zero let shadowView = UIView() - shadowView.backgroundColor = Theme.actionSheetBackgroundColor + shadowView.backgroundColor = forceDarkTheme ? .ows_gray75 : Theme.actionSheetBackgroundColor shadowView.layer.cornerRadius = pickerDiameter / 2 shadowView.layer.shadowColor = UIColor.ows_black.cgColor shadowView.layer.shadowRadius = 12 @@ -96,7 +96,7 @@ class MessageReactionPicker: UIStackView { for (index, emoji) in emojiSet.enumerated() { let button = OWSFlatButton() button.autoSetDimensions(to: CGSize(square: reactionHeight)) - button.setTitle(title: emoji.rawValue, font: .systemFont(ofSize: reactionFontSize), titleColor: Theme.primaryTextColor) + button.setTitle(title: emoji.rawValue, font: .systemFont(ofSize: reactionFontSize), titleColor: forceDarkTheme ? Theme.darkThemePrimaryColor : Theme.primaryTextColor) button.setPressedBlock { [weak self] in // current title of button may have changed in the meantime if let currentEmoji = button.button.title(for: .normal) { @@ -109,7 +109,7 @@ class MessageReactionPicker: UIStackView { // Add a circle behind the currently selected emoji if self.selectedEmoji == emoji { let selectedBackgroundView = UIView() - selectedBackgroundView.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray60 : .ows_gray05 + selectedBackgroundView.backgroundColor = Theme.isDarkThemeEnabled || forceDarkTheme ? .ows_gray60 : .ows_gray05 selectedBackgroundView.clipsToBounds = true selectedBackgroundView.layer.cornerRadius = selectedBackgroundHeight / 2 backgroundView?.addSubview(selectedBackgroundView) @@ -122,7 +122,7 @@ class MessageReactionPicker: UIStackView { if addAnyButton { let button = OWSFlatButton() button.autoSetDimensions(to: CGSize(square: reactionHeight)) - button.setImage(Theme.isDarkThemeEnabled ? #imageLiteral(resourceName: "any-emoji-32-dark") : #imageLiteral(resourceName: "any-emoji-32-light")) + button.setImage(Theme.isDarkThemeEnabled || forceDarkTheme ? #imageLiteral(resourceName: "any-emoji-32-dark") : #imageLiteral(resourceName: "any-emoji-32-light")) button.setPressedBlock { [weak self] in self?.delegate?.didSelectAnyEmoji() } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 68f86ec5ea..ae60940be8 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -6097,6 +6097,30 @@ /* Tooltip prompting the user to visit a story link. */ "STORY_LINK_PREVIEW_VISIT_LINK_TOOLTIP" = "Visit link"; +/* Button for replying to a story with a single existing reply. */ +"STORY_REPLY_BUTTON_WITH_1_REPLY" = "1 reply"; + +/* Button for replying to a story with N existing replies. {embeds the number of replies} */ +"STORY_REPLY_BUTTON_WITH_N_REPLIES_FORMAT" = "%@ replies"; + +/* Button for replying to a story with no existing replies. */ +"STORY_REPLY_BUTTON_WITH_NO_REPLIES" = "Reply"; + +/* Text indicating a story has been reacted to */ +"STORY_REPLY_REACTION" = "Reacted to the story"; + +/* placeholder text for replying to a story */ +"STORY_REPLY_TEXT_FIELD_PLACEHOLDER" = "Reply"; + +/* Author text when the local user author's a story reply */ +"STORY_REPLY_YOU_AUTHOR" = "You"; + +/* Text indicating a story has been reacted to */ +"STORY_REPLY_REACTION" = "Reacted to the story"; + +/* placeholder text for replying to a story */ +"STORY_REPLY_TEXT_FIELD_PLACEHOLDER" = "Reply"; + /* Title for the 'Badges' button in sustainer view. */ "SUBSCRIBER_BADGES" = "Badges"; diff --git a/SignalMessaging/Notifications/AppNotifications.swift b/SignalMessaging/Notifications/AppNotifications.swift index a064df556f..03d1e799b3 100644 --- a/SignalMessaging/Notifications/AppNotifications.swift +++ b/SignalMessaging/Notifications/AppNotifications.swift @@ -426,6 +426,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { public func canNotify(for incomingMessage: TSIncomingMessage, thread: TSThread, transaction: SDSAnyReadTransaction) -> Bool { + guard !incomingMessage.isGroupStoryReply else { return false } + guard isThreadMuted(thread, transaction: transaction) else { return true } diff --git a/SignalMessaging/contacts/OWSContactsManager.swift b/SignalMessaging/contacts/OWSContactsManager.swift index 9ef2421e96..e8cfb2b651 100644 --- a/SignalMessaging/contacts/OWSContactsManager.swift +++ b/SignalMessaging/contacts/OWSContactsManager.swift @@ -984,9 +984,11 @@ extension OWSContactsManager { // Rather than being called once for each address, we call it once with all the // addresses and it will use a single database query per step to assign // display names to addresses using different techniques. - @objc(objc_displayNamesForAddresses:transaction:) - func displayNames(for addresses: [SignalServiceAddress], transaction: SDSAnyReadTransaction) -> [String] { - return Refinery(addresses).refine { addresses in + private func displayNamesRefinery( + for addresses: [SignalServiceAddress], + transaction: SDSAnyReadTransaction + ) -> Refinery { + return .init(addresses).refine { addresses in // Prefer a saved name from system contacts, if available. return cachedContactNames(for: addresses, transaction: transaction) }.refine { addresses in @@ -1009,11 +1011,22 @@ extension OWSContactsManager { self.fetchProfile(forUnknownAddress: $0) return self.unknownUserLabel } - }.values.map { - $0! } } + @objc + public func displayNamesByAddress( + for addresses: [SignalServiceAddress], + transaction: SDSAnyReadTransaction + ) -> [SignalServiceAddress: String] { + Dictionary(displayNamesRefinery(for: addresses, transaction: transaction)) + } + + @objc(objc_displayNamesForAddresses:transaction:) + func displayNames(for addresses: [SignalServiceAddress], transaction: SDSAnyReadTransaction) -> [String] { + displayNamesRefinery(for: addresses, transaction: transaction).values.map { $0! } + } + func phoneNumbers(for addresses: [SignalServiceAddress], transaction: SDSAnyReadTransaction) -> [String?] { return Refinery(addresses).refine { diff --git a/SignalMessaging/utils/ThreadUtil.h b/SignalMessaging/utils/ThreadUtil.h index 7c078e6a12..ed79b73f6d 100644 --- a/SignalMessaging/utils/ThreadUtil.h +++ b/SignalMessaging/utils/ThreadUtil.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -47,6 +47,11 @@ NS_ASSUME_NONNULL_BEGIN transaction:(SDSAnyWriteTransaction *)transaction NS_SWIFT_NAME(addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer(thread:transaction:)); ++ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequest:(TSThread *)thread + setDefaultTimerIfNecessary:(BOOL)setDefaultTimerIfNecessary + transaction:(SDSAnyWriteTransaction *)transaction + NS_SWIFT_NAME(addThreadToProfileWhitelistIfEmptyOrPendingRequest(thread:setDefaultTimerIfNecessary:transaction:)); + #pragma mark - Delete Content + (void)deleteAllContent; diff --git a/SignalMessaging/utils/ThreadUtil.m b/SignalMessaging/utils/ThreadUtil.m index e445a89bd3..22a8e43913 100644 --- a/SignalMessaging/utils/ThreadUtil.m +++ b/SignalMessaging/utils/ThreadUtil.m @@ -182,6 +182,15 @@ NS_ASSUME_NONNULL_BEGIN + (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer:(TSThread *)thread transaction:(SDSAnyWriteTransaction *)transaction +{ + return [self addThreadToProfileWhitelistIfEmptyOrPendingRequest:thread + setDefaultTimerIfNecessary:YES + transaction:transaction]; +} + ++ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequest:(TSThread *)thread + setDefaultTimerIfNecessary:(BOOL)setDefaultTimerIfNecessary + transaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(thread); @@ -192,7 +201,7 @@ NS_ASSUME_NONNULL_BEGIN [GRDBThreadFinder shouldSetDefaultDisappearingMessageTimerWithThread:thread transaction:transaction.unwrapGrdbRead]; - if (needsDefaultTimerSet) { + if (needsDefaultTimerSet && setDefaultTimerIfNecessary) { OWSDisappearingMessagesConfiguration *configuration = [OWSDisappearingMessagesConfiguration applyToken:defaultTimerToken toThread:thread transaction:transaction]; diff --git a/SignalServiceKit/Resources/schema.sql b/SignalServiceKit/Resources/schema.sql index 383a6fc8be..4eff687da6 100644 --- a/SignalServiceKit/Resources/schema.sql +++ b/SignalServiceKit/Resources/schema.sql @@ -115,6 +115,10 @@ CREATE ,"paymentRequest" BLOB ,"viewed" BOOLEAN ,"serverGuid" TEXT + ,"storyAuthorUuidString" TEXT + ,"storyTimestamp" INTEGER + ,"isGroupStoryReply" BOOLEAN + ,"storyReactionEmoji" TEXT ) ; @@ -533,14 +537,6 @@ CREATE ) ; -CREATE - INDEX "index_interactions_unread_counts" - ON "model_TSInteraction"("read" - ,"uniqueThreadId" - ,"recordType" -) -; - CREATE INDEX "index_interactions_on_expiresInSeconds_and_expiresAt" ON "model_TSInteraction"("expiresAt" @@ -1220,28 +1216,6 @@ CREATE ) ; -CREATE - INDEX index_model_TSInteraction_ConversationLoadInteractionCount - ON model_TSInteraction ( - uniqueThreadId - ,recordType - ) -WHERE - recordType IS NOT 70 -; - -CREATE - INDEX index_model_TSInteraction_ConversationLoadInteractionDistance - ON model_TSInteraction ( - uniqueThreadId - ,id - ,recordType - ,uniqueId - ) -WHERE - recordType IS NOT 70 -; - CREATE TABLE IF NOT EXISTS "model_ProfileBadgeTable" ( @@ -1299,3 +1273,37 @@ CREATE ) ) ; + +CREATE + INDEX index_model_TSInteraction_ConversationLoadInteractionCount + ON model_TSInteraction ( + uniqueThreadId + ,isGroupStoryReply + ,recordType + ) +WHERE + recordType IS NOT 70 +; + +CREATE + INDEX index_model_TSInteraction_ConversationLoadInteractionDistance + ON model_TSInteraction ( + uniqueThreadId + ,id + ,isGroupStoryReply + ,recordType + ,uniqueId + ) +WHERE + recordType IS NOT 70 +; + +CREATE + INDEX index_model_TSInteraction_UnreadCount + ON model_TSInteraction ( + READ + ,isGroupStoryReply + ,uniqueThreadId + ,recordType + ) +; diff --git a/SignalServiceKit/src/Contacts/TSThread.h b/SignalServiceKit/src/Contacts/TSThread.h index 4539d3c82d..dcbd0b7488 100644 --- a/SignalServiceKit/src/Contacts/TSThread.h +++ b/SignalServiceKit/src/Contacts/TSThread.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import @@ -92,11 +92,6 @@ NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:conversationColorNa #pragma mark Interactions -/** - * @return The number of interactions in this thread. - */ -- (NSUInteger)numberOfInteractionsWithTransaction:(SDSAnyReadTransaction *)transaction; - /** * Get all messages in the thread we weren't able to decrypt */ diff --git a/SignalServiceKit/src/Contacts/TSThread.m b/SignalServiceKit/src/Contacts/TSThread.m index a6ad42a48c..f69050c4df 100644 --- a/SignalServiceKit/src/Contacts/TSThread.m +++ b/SignalServiceKit/src/Contacts/TSThread.m @@ -354,13 +354,6 @@ lastVisibleSortIdOnScreenPercentageObsolete:(double)lastVisibleSortIdOnScreenPer } #pragma clang diagnostic pop -- (NSUInteger)numberOfInteractionsWithTransaction:(SDSAnyReadTransaction *)transaction -{ - OWSAssertDebug(transaction); - return [[[InteractionFinder alloc] initWithThreadUniqueId:self.uniqueId] countExcludingPlaceholders:NO - transaction:transaction]; -} - - (nullable TSInteraction *)lastInteractionForInboxWithTransaction:(SDSAnyReadTransaction *)transaction { OWSAssertDebug(transaction); diff --git a/SignalServiceKit/src/Contacts/Threads/TSThread+OWS.swift b/SignalServiceKit/src/Contacts/Threads/TSThread+OWS.swift index 273c938450..5b5bcbad4b 100644 --- a/SignalServiceKit/src/Contacts/Threads/TSThread+OWS.swift +++ b/SignalServiceKit/src/Contacts/Threads/TSThread+OWS.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation @@ -171,6 +171,23 @@ public extension TSThread { } lastVisibleInteractionStore.setData(data, key: thread.uniqueId, transaction: transaction) } + + @available(swift, obsoleted: 1.0) + @objc + func numberOfInteractions(transaction: SDSAnyReadTransaction) -> UInt { + numberOfInteractions(transaction: transaction) + } + + func numberOfInteractions( + with storyReplyQueryMode: StoryReplyQueryMode = .excludeGroupReplies, + transaction: SDSAnyReadTransaction + ) -> UInt { + InteractionFinder(threadUniqueId: uniqueId).count( + excludingPlaceholders: false, + storyReplyQueryMode: storyReplyQueryMode, + transaction: transaction + ) + } } // MARK: - Drafts diff --git a/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m b/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m index f5421c3d45..059940b3ee 100644 --- a/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m +++ b/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m @@ -116,7 +116,10 @@ NS_ASSUME_NONNULL_BEGIN messageSticker:transcript.messageSticker isViewOnceMessage:transcript.isViewOnceMessage changeActionsProtoData:nil - additionalRecipients:nil] build]; + additionalRecipients:nil + storyAuthorAddress:transcript.storyAuthorAddress + storyTimestamp:transcript.storyTimestamp + storyReactionEmoji:nil] build]; SignalServiceAddress *_Nullable localAddress = self.tsAccountManager.localAddress; if (localAddress == nil) { diff --git a/SignalServiceKit/src/Messages/DeviceSyncing/OWSIncomingSentMessageTranscript.h b/SignalServiceKit/src/Messages/DeviceSyncing/OWSIncomingSentMessageTranscript.h index aea3eb6e6a..05eb0896bd 100644 --- a/SignalServiceKit/src/Messages/DeviceSyncing/OWSIncomingSentMessageTranscript.h +++ b/SignalServiceKit/src/Messages/DeviceSyncing/OWSIncomingSentMessageTranscript.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -55,6 +55,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, nullable) TSPaymentCancellation *paymentCancellation; @property (nonatomic, readonly, nullable) NSNumber *requiredProtocolVersion; @property (nonatomic, readonly) DisappearingMessageToken *disappearingMessageToken; +@property (nonatomic, readonly, nullable) NSNumber *storyTimestamp; +@property (nonatomic, readonly, nullable) SignalServiceAddress *storyAuthorAddress; // If either nonUdRecipientIds or udRecipientIds is nil, // this is either a legacy transcript or it reflects a legacy sync message. diff --git a/SignalServiceKit/src/Messages/DeviceSyncing/OWSIncomingSentMessageTranscript.m b/SignalServiceKit/src/Messages/DeviceSyncing/OWSIncomingSentMessageTranscript.m index 7778bc1ffd..6d5209c007 100644 --- a/SignalServiceKit/src/Messages/DeviceSyncing/OWSIncomingSentMessageTranscript.m +++ b/SignalServiceKit/src/Messages/DeviceSyncing/OWSIncomingSentMessageTranscript.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import "OWSIncomingSentMessageTranscript.h" @@ -222,6 +222,18 @@ NS_ASSUME_NONNULL_BEGIN _paymentRequest = paymentModels.request; _paymentNotification = paymentModels.notification; _paymentCancellation = paymentModels.cancellation; + + if (_dataMessage.storyContext != nil && _dataMessage.storyContext.hasSentTimestamp + && _dataMessage.storyContext.hasAuthorUuid) { + _storyTimestamp = @(_dataMessage.storyContext.sentTimestamp); + _storyAuthorAddress = + [[SignalServiceAddress alloc] initWithUuidString:_dataMessage.storyContext.authorUuid]; + + if (!_storyAuthorAddress.isValid) { + OWSFailDebug(@"Discarding story reply transcript with invalid address %@", _storyAuthorAddress); + return nil; + } + } } if (sentProto.unidentifiedStatus.count > 0) { diff --git a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage+SDS.swift b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage+SDS.swift index afadefffd5..e76bfc02ca 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage+SDS.swift @@ -125,7 +125,11 @@ class OWSDisappearingConfigurationUpdateInfoMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.h b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.h index 50d39285a9..dad78a215e 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import @@ -67,12 +67,16 @@ NS_ASSUME_NONNULL_BEGIN expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage infoMessageUserInfo:(nullable NSDictionary *)infoMessageUserInfo @@ -83,7 +87,7 @@ NS_ASSUME_NONNULL_BEGIN configurationIsEnabled:(BOOL)configurationIsEnabled createdByRemoteName:(nullable NSString *)createdByRemoteName createdInExistingGroup:(BOOL)createdInExistingGroup -NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:wasRemotelyDeleted:customMessage:infoMessageUserInfo:messageType:read:unregisteredAddress:configurationDurationSeconds:configurationIsEnabled:createdByRemoteName:createdInExistingGroup:)); +NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isGroupStoryReply:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:storyAuthorUuidString:storyReactionEmoji:storyTimestamp:wasRemotelyDeleted:customMessage:infoMessageUserInfo:messageType:read:unregisteredAddress:configurationDurationSeconds:configurationIsEnabled:createdByRemoteName:createdInExistingGroup:)); // clang-format on diff --git a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m index 737c2c7915..f7e4f4ff96 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import "OWSDisappearingConfigurationUpdateInfoMessage.h" @@ -67,12 +67,16 @@ NS_ASSUME_NONNULL_BEGIN expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage infoMessageUserInfo:(nullable NSDictionary *)infoMessageUserInfo @@ -97,12 +101,16 @@ NS_ASSUME_NONNULL_BEGIN expireStartedAt:expireStartedAt expiresAt:expiresAt expiresInSeconds:expiresInSeconds + isGroupStoryReply:isGroupStoryReply isViewOnceComplete:isViewOnceComplete isViewOnceMessage:isViewOnceMessage linkPreview:linkPreview messageSticker:messageSticker quotedMessage:quotedMessage storedShouldStartExpireTimer:storedShouldStartExpireTimer + storyAuthorUuidString:storyAuthorUuidString + storyReactionEmoji:storyReactionEmoji + storyTimestamp:storyTimestamp wasRemotelyDeleted:wasRemotelyDeleted customMessage:customMessage infoMessageUserInfo:infoMessageUserInfo diff --git a/SignalServiceKit/src/Messages/Interactions/OWSVerificationStateChangeMessage+SDS.swift b/SignalServiceKit/src/Messages/Interactions/OWSVerificationStateChangeMessage+SDS.swift index 9b0d914852..cca8b9a6e6 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSVerificationStateChangeMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/Interactions/OWSVerificationStateChangeMessage+SDS.swift @@ -125,7 +125,11 @@ class OWSVerificationStateChangeMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/Interactions/OWSVerificationStateChangeMessage.h b/SignalServiceKit/src/Messages/Interactions/OWSVerificationStateChangeMessage.h index 3b2e9e6e58..39746bd9a1 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSVerificationStateChangeMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/OWSVerificationStateChangeMessage.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import @@ -67,12 +67,16 @@ NS_ASSUME_NONNULL_BEGIN expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage infoMessageUserInfo:(nullable NSDictionary *)infoMessageUserInfo @@ -82,7 +86,7 @@ NS_ASSUME_NONNULL_BEGIN isLocalChange:(BOOL)isLocalChange recipientAddress:(SignalServiceAddress *)recipientAddress verificationState:(OWSVerificationState)verificationState -NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:wasRemotelyDeleted:customMessage:infoMessageUserInfo:messageType:read:unregisteredAddress:isLocalChange:recipientAddress:verificationState:)); +NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isGroupStoryReply:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:storyAuthorUuidString:storyReactionEmoji:storyTimestamp:wasRemotelyDeleted:customMessage:infoMessageUserInfo:messageType:read:unregisteredAddress:isLocalChange:recipientAddress:verificationState:)); // clang-format on diff --git a/SignalServiceKit/src/Messages/Interactions/OWSVerificationStateChangeMessage.m b/SignalServiceKit/src/Messages/Interactions/OWSVerificationStateChangeMessage.m index 145fa911a6..01fd066128 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSVerificationStateChangeMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/OWSVerificationStateChangeMessage.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import "OWSVerificationStateChangeMessage.h" @@ -61,12 +61,16 @@ NS_ASSUME_NONNULL_BEGIN expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage infoMessageUserInfo:(nullable NSDictionary *)infoMessageUserInfo @@ -90,12 +94,16 @@ NS_ASSUME_NONNULL_BEGIN expireStartedAt:expireStartedAt expiresAt:expiresAt expiresInSeconds:expiresInSeconds + isGroupStoryReply:isGroupStoryReply isViewOnceComplete:isViewOnceComplete isViewOnceMessage:isViewOnceMessage linkPreview:linkPreview messageSticker:messageSticker quotedMessage:quotedMessage storedShouldStartExpireTimer:storedShouldStartExpireTimer + storyAuthorUuidString:storyAuthorUuidString + storyReactionEmoji:storyReactionEmoji + storyTimestamp:storyTimestamp wasRemotelyDeleted:wasRemotelyDeleted customMessage:customMessage infoMessageUserInfo:infoMessageUserInfo diff --git a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage+SDS.swift b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage+SDS.swift index ff289e9282..c36d16688c 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage+SDS.swift @@ -125,7 +125,11 @@ class TSErrorMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.h b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.h index 628c2dab79..b03a30979b 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import @@ -88,19 +88,23 @@ extern NSUInteger TSErrorMessageSchemaVersion; expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted errorType:(TSErrorMessageType)errorType read:(BOOL)read recipientAddress:(nullable SignalServiceAddress *)recipientAddress sender:(nullable SignalServiceAddress *)sender wasIdentityVerified:(BOOL)wasIdentityVerified -NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:wasRemotelyDeleted:errorType:read:recipientAddress:sender:wasIdentityVerified:)); +NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isGroupStoryReply:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:storyAuthorUuidString:storyReactionEmoji:storyTimestamp:wasRemotelyDeleted:errorType:read:recipientAddress:sender:wasIdentityVerified:)); // clang-format on diff --git a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m index 690b1a98b4..38f497a8e8 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import "TSErrorMessage.h" @@ -136,12 +136,16 @@ NSUInteger TSErrorMessageSchemaVersion = 2; expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted errorType:(TSErrorMessageType)errorType read:(BOOL)read @@ -162,12 +166,16 @@ NSUInteger TSErrorMessageSchemaVersion = 2; expireStartedAt:expireStartedAt expiresAt:expiresAt expiresInSeconds:expiresInSeconds + isGroupStoryReply:isGroupStoryReply isViewOnceComplete:isViewOnceComplete isViewOnceMessage:isViewOnceMessage linkPreview:linkPreview messageSticker:messageSticker quotedMessage:quotedMessage storedShouldStartExpireTimer:storedShouldStartExpireTimer + storyAuthorUuidString:storyAuthorUuidString + storyReactionEmoji:storyReactionEmoji + storyTimestamp:storyTimestamp wasRemotelyDeleted:wasRemotelyDeleted]; if (!self) { diff --git a/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage+Builder.swift b/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage+Builder.swift index 454bcaabed..447064f0c6 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage+Builder.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage+Builder.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation @@ -39,7 +39,10 @@ public class TSIncomingMessageBuilder: TSMessageBuilder { serverDeliveryTimestamp: UInt64 = 0, serverGuid: String? = nil, wasReceivedByUD: Bool = false, - isViewOnceMessage: Bool = false) { + isViewOnceMessage: Bool = false, + storyAuthorAddress: SignalServiceAddress? = nil, + storyTimestamp: UInt64? = nil, + storyReactionEmoji: String? = nil) { super.init(thread: thread, timestamp: timestamp, @@ -48,12 +51,15 @@ public class TSIncomingMessageBuilder: TSMessageBuilder { attachmentIds: attachmentIds, expiresInSeconds: expiresInSeconds, // expireStartedAt is always initialized to zero for incoming messages. - expireStartedAt: 0, - quotedMessage: quotedMessage, - contactShare: contactShare, - linkPreview: linkPreview, - messageSticker: messageSticker, - isViewOnceMessage: isViewOnceMessage) + expireStartedAt: 0, + quotedMessage: quotedMessage, + contactShare: contactShare, + linkPreview: linkPreview, + messageSticker: messageSticker, + isViewOnceMessage: isViewOnceMessage, + storyAuthorAddress: storyAuthorAddress, + storyTimestamp: storyTimestamp, + storyReactionEmoji: storyReactionEmoji) self.authorAddress = authorAddress self.sourceDeviceId = sourceDeviceId @@ -95,7 +101,10 @@ public class TSIncomingMessageBuilder: TSMessageBuilder { serverDeliveryTimestamp: UInt64, serverGuid: String?, wasReceivedByUD: Bool, - isViewOnceMessage: Bool) -> TSIncomingMessageBuilder { + isViewOnceMessage: Bool, + storyAuthorAddress: SignalServiceAddress?, + storyTimestamp: NSNumber?, + storyReactionEmoji: String?) -> TSIncomingMessageBuilder { return TSIncomingMessageBuilder(thread: thread, timestamp: timestamp, authorAddress: authorAddress, @@ -112,7 +121,10 @@ public class TSIncomingMessageBuilder: TSMessageBuilder { serverDeliveryTimestamp: serverDeliveryTimestamp, serverGuid: serverGuid, wasReceivedByUD: wasReceivedByUD, - isViewOnceMessage: isViewOnceMessage) + isViewOnceMessage: isViewOnceMessage, + storyAuthorAddress: storyAuthorAddress, + storyTimestamp: storyTimestamp?.uint64Value, + storyReactionEmoji: storyReactionEmoji) } private var hasBuilt = false diff --git a/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage+SDS.swift b/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage+SDS.swift index 26bfea037b..769e47ba1c 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage+SDS.swift @@ -125,7 +125,11 @@ class TSIncomingMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = model.wasViewed let serverGuid: String? = model.serverGuid + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.h b/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.h index a3e261d3d6..156264ae34 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import @@ -67,12 +67,16 @@ NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(incomingMessageWithBuilder:)); expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted authorPhoneNumber:(nullable NSString *)authorPhoneNumber authorUUID:(nullable NSString *)authorUUID @@ -83,7 +87,7 @@ NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(incomingMessageWithBuilder:)); sourceDeviceId:(unsigned int)sourceDeviceId viewed:(BOOL)viewed wasReceivedByUD:(BOOL)wasReceivedByUD -NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:wasRemotelyDeleted:authorPhoneNumber:authorUUID:read:serverDeliveryTimestamp:serverGuid:serverTimestamp:sourceDeviceId:viewed:wasReceivedByUD:)); +NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isGroupStoryReply:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:storyAuthorUuidString:storyReactionEmoji:storyTimestamp:wasRemotelyDeleted:authorPhoneNumber:authorUUID:read:serverDeliveryTimestamp:serverGuid:serverTimestamp:sourceDeviceId:viewed:wasReceivedByUD:)); // clang-format on diff --git a/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.m b/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.m index 1bfc0a7f61..78e68f7f60 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import "TSIncomingMessage.h" @@ -91,12 +91,16 @@ const NSUInteger TSIncomingMessageSchemaVersion = 1; expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted authorPhoneNumber:(nullable NSString *)authorPhoneNumber authorUUID:(nullable NSString *)authorUUID @@ -121,12 +125,16 @@ const NSUInteger TSIncomingMessageSchemaVersion = 1; expireStartedAt:expireStartedAt expiresAt:expiresAt expiresInSeconds:expiresInSeconds + isGroupStoryReply:isGroupStoryReply isViewOnceComplete:isViewOnceComplete isViewOnceMessage:isViewOnceMessage linkPreview:linkPreview messageSticker:messageSticker quotedMessage:quotedMessage storedShouldStartExpireTimer:storedShouldStartExpireTimer + storyAuthorUuidString:storyAuthorUuidString + storyReactionEmoji:storyReactionEmoji + storyTimestamp:storyTimestamp wasRemotelyDeleted:wasRemotelyDeleted]; if (!self) { diff --git a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage+SDS.swift b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage+SDS.swift index d4f2a4110e..4a40475276 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage+SDS.swift @@ -125,7 +125,11 @@ class TSInfoMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.h b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.h index 2bca16bb98..ebfcfe02f8 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import @@ -109,19 +109,23 @@ extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyChangePhoneNumberNew; expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage infoMessageUserInfo:(nullable NSDictionary *)infoMessageUserInfo messageType:(TSInfoMessageType)messageType read:(BOOL)read unregisteredAddress:(nullable SignalServiceAddress *)unregisteredAddress -NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:wasRemotelyDeleted:customMessage:infoMessageUserInfo:messageType:read:unregisteredAddress:)); +NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isGroupStoryReply:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:storyAuthorUuidString:storyReactionEmoji:storyTimestamp:wasRemotelyDeleted:customMessage:infoMessageUserInfo:messageType:read:unregisteredAddress:)); // clang-format on diff --git a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.m b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.m index 568382438f..58ac724f04 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import "TSInfoMessage.h" @@ -141,12 +141,16 @@ NSUInteger TSInfoMessageSchemaVersion = 2; expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage infoMessageUserInfo:(nullable NSDictionary *)infoMessageUserInfo @@ -167,12 +171,16 @@ NSUInteger TSInfoMessageSchemaVersion = 2; expireStartedAt:expireStartedAt expiresAt:expiresAt expiresInSeconds:expiresInSeconds + isGroupStoryReply:isGroupStoryReply isViewOnceComplete:isViewOnceComplete isViewOnceMessage:isViewOnceMessage linkPreview:linkPreview messageSticker:messageSticker quotedMessage:quotedMessage storedShouldStartExpireTimer:storedShouldStartExpireTimer + storyAuthorUuidString:storyAuthorUuidString + storyReactionEmoji:storyReactionEmoji + storyTimestamp:storyTimestamp wasRemotelyDeleted:wasRemotelyDeleted]; if (!self) { diff --git a/SignalServiceKit/src/Messages/Interactions/TSInteraction+SDS.swift b/SignalServiceKit/src/Messages/Interactions/TSInteraction+SDS.swift index 7f904edec9..7082c6b7d3 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInteraction+SDS.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSInteraction+SDS.swift @@ -94,6 +94,10 @@ public struct InteractionRecord: SDSRecord { public let paymentRequest: Data? public let viewed: Bool? public let serverGuid: String? + public let storyAuthorUuidString: String? + public let storyTimestamp: UInt64? + public let isGroupStoryReply: Bool? + public let storyReactionEmoji: String? public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { case id @@ -163,6 +167,10 @@ public struct InteractionRecord: SDSRecord { case paymentRequest case viewed case serverGuid + case storyAuthorUuidString + case storyTimestamp + case isGroupStoryReply + case storyReactionEmoji } public static func columnName(_ column: InteractionRecord.CodingKeys, fullyQualified: Bool = false) -> String { @@ -253,6 +261,10 @@ public extension InteractionRecord { paymentRequest = row[64] viewed = row[65] serverGuid = row[66] + storyAuthorUuidString = row[67] + storyTimestamp = row[68] + isGroupStoryReply = row[69] + storyReactionEmoji = row[70] } } @@ -298,6 +310,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -307,6 +320,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") let customMessage: String? = record.customMessage let infoMessageUserInfoSerialized: Data? = record.infoMessageUserInfo @@ -331,12 +347,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, infoMessageUserInfo: infoMessageUserInfo, @@ -361,6 +381,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -370,6 +391,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") let customMessage: String? = record.customMessage let infoMessageUserInfoSerialized: Data? = record.infoMessageUserInfo @@ -394,12 +418,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, infoMessageUserInfo: infoMessageUserInfo, @@ -424,6 +452,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -433,6 +462,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") let customMessage: String? = record.customMessage let infoMessageUserInfoSerialized: Data? = record.infoMessageUserInfo @@ -461,12 +493,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, infoMessageUserInfo: infoMessageUserInfo, @@ -521,6 +557,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -530,6 +567,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") let customMessage: String? = record.customMessage guard let groupMetaMessage: TSGroupMetaMessage = record.groupMetaMessage else { @@ -569,12 +609,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, groupMetaMessage: groupMetaMessage, @@ -608,6 +652,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -617,6 +662,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") guard let errorType: TSErrorMessageType = record.errorType else { throw SDSError.missingRequiredField @@ -641,12 +689,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, errorType: errorType, read: read, @@ -671,6 +723,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -680,6 +733,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") guard let errorType: TSErrorMessageType = record.errorType else { throw SDSError.missingRequiredField @@ -704,12 +760,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, errorType: errorType, read: read, @@ -734,6 +794,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -743,6 +804,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") let customMessage: String? = record.customMessage let infoMessageUserInfoSerialized: Data? = record.infoMessageUserInfo @@ -770,12 +834,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, infoMessageUserInfo: infoMessageUserInfo, @@ -802,6 +870,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -811,6 +880,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") let customMessage: String? = record.customMessage let infoMessageUserInfoSerialized: Data? = record.infoMessageUserInfo @@ -841,12 +913,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, infoMessageUserInfo: infoMessageUserInfo, @@ -899,6 +975,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -908,6 +985,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") guard let errorType: TSErrorMessageType = record.errorType else { throw SDSError.missingRequiredField @@ -932,12 +1012,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, errorType: errorType, read: read, @@ -962,6 +1046,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -971,6 +1056,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") let authorPhoneNumber: String? = record.authorPhoneNumber let authorUUID: String? = record.authorUUID @@ -995,12 +1083,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, @@ -1029,6 +1121,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -1038,6 +1131,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") let customMessage: String? = record.customMessage let infoMessageUserInfoSerialized: Data? = record.infoMessageUserInfo @@ -1062,12 +1158,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, infoMessageUserInfo: infoMessageUserInfo, @@ -1107,6 +1207,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -1116,6 +1217,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") guard let errorType: TSErrorMessageType = record.errorType else { throw SDSError.missingRequiredField @@ -1140,12 +1244,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, errorType: errorType, read: read, @@ -1170,6 +1278,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -1179,6 +1288,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") guard let errorType: TSErrorMessageType = record.errorType else { throw SDSError.missingRequiredField @@ -1205,12 +1317,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, errorType: errorType, read: read, @@ -1237,6 +1353,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -1246,6 +1363,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") guard let errorType: TSErrorMessageType = record.errorType else { throw SDSError.missingRequiredField @@ -1273,12 +1393,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, errorType: errorType, read: read, @@ -1305,6 +1429,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -1314,6 +1439,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") return TSMessage(grdbId: recordId, @@ -1329,12 +1457,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted) case .outgoingMessage: @@ -1354,6 +1486,7 @@ extension TSInteraction { let expireStartedAt: UInt64 = try SDSDeserialization.required(record.expireStartedAt, name: "expireStartedAt") let expiresAt: UInt64 = try SDSDeserialization.required(record.expiresAt, name: "expiresAt") let expiresInSeconds: UInt32 = try SDSDeserialization.required(record.expiresInSeconds, name: "expiresInSeconds") + let isGroupStoryReply: Bool = try SDSDeserialization.required(record.isGroupStoryReply, name: "isGroupStoryReply") let isViewOnceComplete: Bool = try SDSDeserialization.required(record.isViewOnceComplete, name: "isViewOnceComplete") let isViewOnceMessage: Bool = try SDSDeserialization.required(record.isViewOnceMessage, name: "isViewOnceMessage") let linkPreviewSerialized: Data? = record.linkPreview @@ -1363,6 +1496,9 @@ extension TSInteraction { let quotedMessageSerialized: Data? = record.quotedMessage let quotedMessage: TSQuotedMessage? = try SDSDeserialization.optionalUnarchive(quotedMessageSerialized, name: "quotedMessage") let storedShouldStartExpireTimer: Bool = try SDSDeserialization.required(record.storedShouldStartExpireTimer, name: "storedShouldStartExpireTimer") + let storyAuthorUuidString: String? = record.storyAuthorUuidString + let storyReactionEmoji: String? = record.storyReactionEmoji + let storyTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.storyTimestamp, name: "storyTimestamp", conversion: { NSNumber(value: $0) }) let wasRemotelyDeleted: Bool = try SDSDeserialization.required(record.wasRemotelyDeleted, name: "wasRemotelyDeleted") let customMessage: String? = record.customMessage guard let groupMetaMessage: TSGroupMetaMessage = record.groupMetaMessage else { @@ -1396,12 +1532,16 @@ extension TSInteraction { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, groupMetaMessage: groupMetaMessage, @@ -1588,6 +1728,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -1630,6 +1771,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let customMessage: String? = modelToCopy.customMessage let groupMetaMessage: TSGroupMetaMessage = modelToCopy.groupMetaMessage @@ -1707,12 +1851,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, groupMetaMessage: groupMetaMessage, @@ -1770,6 +1918,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -1812,6 +1961,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let customMessage: String? = modelToCopy.customMessage let groupMetaMessage: TSGroupMetaMessage = modelToCopy.groupMetaMessage @@ -1850,12 +2002,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, groupMetaMessage: groupMetaMessage, @@ -1910,6 +2066,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -1952,6 +2109,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let customMessage: String? = modelToCopy.customMessage // NOTE: If this generates build errors, you made need to @@ -2001,12 +2161,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, infoMessageUserInfo: infoMessageUserInfo, @@ -2058,6 +2222,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -2100,6 +2265,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let customMessage: String? = modelToCopy.customMessage // NOTE: If this generates build errors, you made need to @@ -2158,12 +2326,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, infoMessageUserInfo: infoMessageUserInfo, @@ -2214,6 +2386,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -2256,6 +2429,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let customMessage: String? = modelToCopy.customMessage // NOTE: If this generates build errors, you made need to @@ -2304,12 +2480,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, infoMessageUserInfo: infoMessageUserInfo, @@ -2362,6 +2542,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -2404,6 +2585,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let customMessage: String? = modelToCopy.customMessage // NOTE: If this generates build errors, you made need to @@ -2448,12 +2632,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, infoMessageUserInfo: infoMessageUserInfo, @@ -2502,6 +2690,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -2544,6 +2733,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let customMessage: String? = modelToCopy.customMessage // NOTE: If this generates build errors, you made need to @@ -2588,12 +2780,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, infoMessageUserInfo: infoMessageUserInfo, @@ -2642,6 +2838,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -2684,6 +2881,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let customMessage: String? = modelToCopy.customMessage // NOTE: If this generates build errors, you made need to @@ -2728,12 +2928,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, customMessage: customMessage, infoMessageUserInfo: infoMessageUserInfo, @@ -2782,6 +2986,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -2824,6 +3029,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let authorPhoneNumber: String? = modelToCopy.authorPhoneNumber let authorUUID: String? = modelToCopy.authorUUID @@ -2848,12 +3056,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, @@ -2906,6 +3118,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -2948,6 +3161,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let errorType: TSErrorMessageType = modelToCopy.errorType let read: Bool = modelToCopy.wasRead @@ -2996,12 +3212,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, errorType: errorType, read: read, @@ -3052,6 +3272,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -3094,6 +3315,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let errorType: TSErrorMessageType = modelToCopy.errorType let read: Bool = modelToCopy.wasRead @@ -3140,12 +3364,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, errorType: errorType, read: read, @@ -3196,6 +3424,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -3238,6 +3467,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let errorType: TSErrorMessageType = modelToCopy.errorType let read: Bool = modelToCopy.wasRead @@ -3282,12 +3514,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, errorType: errorType, read: read, @@ -3336,6 +3572,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -3378,6 +3615,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let errorType: TSErrorMessageType = modelToCopy.errorType let read: Bool = modelToCopy.wasRead @@ -3422,12 +3662,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, errorType: errorType, read: read, @@ -3476,6 +3720,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -3518,6 +3763,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let errorType: TSErrorMessageType = modelToCopy.errorType let read: Bool = modelToCopy.wasRead @@ -3562,12 +3810,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, errorType: errorType, read: read, @@ -3616,6 +3868,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -3658,6 +3911,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted let errorType: TSErrorMessageType = modelToCopy.errorType let read: Bool = modelToCopy.wasRead @@ -3702,12 +3958,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted, errorType: errorType, read: read, @@ -3756,6 +4016,7 @@ extension TSInteraction: DeepCopyable { let expireStartedAt: UInt64 = modelToCopy.expireStartedAt let expiresAt: UInt64 = modelToCopy.expiresAt let expiresInSeconds: UInt32 = modelToCopy.expiresInSeconds + let isGroupStoryReply: Bool = modelToCopy.isGroupStoryReply let isViewOnceComplete: Bool = modelToCopy.isViewOnceComplete let isViewOnceMessage: Bool = modelToCopy.isViewOnceMessage // NOTE: If this generates build errors, you made need to @@ -3798,6 +4059,9 @@ extension TSInteraction: DeepCopyable { quotedMessage = nil } let storedShouldStartExpireTimer: Bool = modelToCopy.storedShouldStartExpireTimer + let storyAuthorUuidString: String? = modelToCopy.storyAuthorUuidString + let storyReactionEmoji: String? = modelToCopy.storyReactionEmoji + let storyTimestamp: NSNumber? = modelToCopy.storyTimestamp let wasRemotelyDeleted: Bool = modelToCopy.wasRemotelyDeleted return TSMessage(grdbId: id, @@ -3813,12 +4077,16 @@ extension TSInteraction: DeepCopyable { expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, + isGroupStoryReply: isGroupStoryReply, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, linkPreview: linkPreview, messageSticker: messageSticker, quotedMessage: quotedMessage, storedShouldStartExpireTimer: storedShouldStartExpireTimer, + storyAuthorUuidString: storyAuthorUuidString, + storyReactionEmoji: storyReactionEmoji, + storyTimestamp: storyTimestamp, wasRemotelyDeleted: wasRemotelyDeleted) } @@ -3976,6 +4244,10 @@ extension TSInteractionSerializer { static var paymentRequestColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "paymentRequest", columnType: .blob, isOptional: true) } static var viewedColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "viewed", columnType: .int, isOptional: true) } static var serverGuidColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "serverGuid", columnType: .unicodeString, isOptional: true) } + static var storyAuthorUuidStringColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "storyAuthorUuidString", columnType: .unicodeString, isOptional: true) } + static var storyTimestampColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "storyTimestamp", columnType: .int64, isOptional: true) } + static var isGroupStoryReplyColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "isGroupStoryReply", columnType: .int, isOptional: true) } + static var storyReactionEmojiColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "storyReactionEmoji", columnType: .unicodeString, isOptional: true) } // TODO: We should decide on a naming convention for // tables that store models. @@ -4049,7 +4321,11 @@ extension TSInteractionSerializer { paymentNotificationColumn, paymentRequestColumn, viewedColumn, - serverGuidColumn + serverGuidColumn, + storyAuthorUuidStringColumn, + storyTimestampColumn, + isGroupStoryReplyColumn, + storyReactionEmojiColumn ]) } } @@ -4520,8 +4796,12 @@ class TSInteractionSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = nil + let storyTimestamp: UInt64? = nil + let isGroupStoryReply: Bool? = nil + let storyReactionEmoji: String? = nil - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/Interactions/TSInteraction.swift b/SignalServiceKit/src/Messages/Interactions/TSInteraction.swift index fa57fc8b6d..f46db45829 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInteraction.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSInteraction.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation @@ -79,7 +79,8 @@ extension TSInteraction { default: return true } - + case let message as TSMessage: + return !message.isGroupStoryReply default: return true } diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage+Builder.swift b/SignalServiceKit/src/Messages/Interactions/TSMessage+Builder.swift index 8023f77b7a..91a289a5ca 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage+Builder.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage+Builder.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation @@ -35,6 +35,16 @@ public class TSMessageBuilder: NSObject { public var messageSticker: MessageSticker? @objc public var isViewOnceMessage = false + @objc + public var storyAuthorAddress: SignalServiceAddress? + @objc + public var storyTimestamp: NSNumber? + @objc + public var storyReactionEmoji: String? + @objc + public var isGroupStoryReply: Bool { + storyAuthorAddress != nil && storyTimestamp != nil && thread.isGroupThread + } init(thread: TSThread, timestamp: UInt64? = nil, @@ -47,7 +57,10 @@ public class TSMessageBuilder: NSObject { contactShare: OWSContact? = nil, linkPreview: OWSLinkPreview? = nil, messageSticker: MessageSticker? = nil, - isViewOnceMessage: Bool = false) { + isViewOnceMessage: Bool = false, + storyAuthorAddress: SignalServiceAddress? = nil, + storyTimestamp: UInt64? = nil, + storyReactionEmoji: String? = nil) { self.thread = thread if let timestamp = timestamp { @@ -65,6 +78,9 @@ public class TSMessageBuilder: NSObject { self.linkPreview = linkPreview self.messageSticker = messageSticker self.isViewOnceMessage = isViewOnceMessage + self.storyAuthorAddress = storyAuthorAddress + self.storyTimestamp = storyTimestamp.map { NSNumber(value: $0) } + self.storyReactionEmoji = storyReactionEmoji } @objc diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage+SDS.swift b/SignalServiceKit/src/Messages/Interactions/TSMessage+SDS.swift index 400a944b1e..1a29352f8d 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage+SDS.swift @@ -125,7 +125,11 @@ class TSMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.h b/SignalServiceKit/src/Messages/Interactions/TSMessage.h index b7ef385759..c195bb59ba 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import @@ -46,6 +46,14 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) BOOL isViewOnceComplete; @property (nonatomic, readonly) BOOL wasRemotelyDeleted; +// Story Context +@property (nonatomic, readonly, nullable) NSNumber *storyTimestamp; +@property (nonatomic, readonly, nullable) SignalServiceAddress *storyAuthorAddress; +@property (nonatomic, readonly, nullable) NSString *storyAuthorUuidString; +@property (nonatomic, readonly) BOOL isGroupStoryReply; +@property (nonatomic, readonly) BOOL isStoryReply; +@property (nonatomic, readonly, nullable) NSString *storyReactionEmoji; + - (instancetype)initWithUniqueId:(NSString *)uniqueId timestamp:(uint64_t)timestamp thread:(TSThread *)thread NS_UNAVAILABLE; @@ -85,14 +93,18 @@ NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(messageWithBuilder:)); expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted -NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:wasRemotelyDeleted:)); +NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isGroupStoryReply:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:storyAuthorUuidString:storyReactionEmoji:storyTimestamp:wasRemotelyDeleted:)); // clang-format on diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.m b/SignalServiceKit/src/Messages/Interactions/TSMessage.m index 8288b40a62..d0a14f14b4 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import "TSMessage.h" @@ -58,6 +58,8 @@ static const NSUInteger OWSMessageSchemaVersion = 4; @property (nonatomic) BOOL isViewOnceComplete; @property (nonatomic) BOOL wasRemotelyDeleted; +@property (nonatomic, nullable) NSString *storyReactionEmoji; + // This property is only intended to be used by GRDB queries. @property (nonatomic, readonly) BOOL storedShouldStartExpireTimer; @@ -92,6 +94,10 @@ static const NSUInteger OWSMessageSchemaVersion = 4; _messageSticker = messageBuilder.messageSticker; _isViewOnceMessage = messageBuilder.isViewOnceMessage; _isViewOnceComplete = NO; + _storyTimestamp = messageBuilder.storyTimestamp; + _storyAuthorUuidString = messageBuilder.storyAuthorAddress.uuidString; + _storyReactionEmoji = messageBuilder.storyReactionEmoji; + _isGroupStoryReply = messageBuilder.isGroupStoryReply; #ifdef DEBUG [self verifyPerConversationExpiration]; @@ -119,12 +125,16 @@ static const NSUInteger OWSMessageSchemaVersion = 4; expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted { self = [super initWithGrdbId:grdbId @@ -145,12 +155,16 @@ static const NSUInteger OWSMessageSchemaVersion = 4; _expireStartedAt = expireStartedAt; _expiresAt = expiresAt; _expiresInSeconds = expiresInSeconds; + _isGroupStoryReply = isGroupStoryReply; _isViewOnceComplete = isViewOnceComplete; _isViewOnceMessage = isViewOnceMessage; _linkPreview = linkPreview; _messageSticker = messageSticker; _quotedMessage = quotedMessage; _storedShouldStartExpireTimer = storedShouldStartExpireTimer; + _storyAuthorUuidString = storyAuthorUuidString; + _storyReactionEmoji = storyReactionEmoji; + _storyTimestamp = storyTimestamp; _wasRemotelyDeleted = wasRemotelyDeleted; [self sdsFinalizeMessage]; @@ -298,6 +312,18 @@ static const NSUInteger OWSMessageSchemaVersion = 4; } } +#pragma mark - Story Context + +- (nullable SignalServiceAddress *)storyAuthorAddress +{ + return [[SignalServiceAddress alloc] initWithUuidString:self.storyAuthorUuidString]; +} + +- (BOOL)isStoryReply +{ + return self.storyAuthorUuidString != nil && self.storyTimestamp != nil; +} + #pragma mark - Attachments - (BOOL)hasAttachments @@ -585,6 +611,8 @@ static const NSUInteger OWSMessageSchemaVersion = 4; [super anyDidInsertWithTransaction:transaction]; [self ensurePerConversationExpirationWithTransaction:transaction]; + + [self touchStoryMessageIfNecessaryWithTransaction:transaction]; } - (void)anyWillUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction @@ -603,6 +631,8 @@ static const NSUInteger OWSMessageSchemaVersion = 4; [super anyDidUpdateWithTransaction:transaction]; [self ensurePerConversationExpirationWithTransaction:transaction]; + + [self touchStoryMessageIfNecessaryWithTransaction:transaction]; } - (void)ensurePerConversationExpirationWithTransaction:(SDSAnyWriteTransaction *)transaction @@ -650,6 +680,22 @@ static const NSUInteger OWSMessageSchemaVersion = 4; [self removeAllReactionsWithTransaction:transaction]; [self removeAllMentionsWithTransaction:transaction]; + + [self touchStoryMessageIfNecessaryWithTransaction:transaction]; +} + +- (void)touchStoryMessageIfNecessaryWithTransaction:(SDSAnyWriteTransaction *)transaction +{ + if (!self.isStoryReply) { + return; + } + + StoryMessage *_Nullable storyMessage = [StoryFinder storyWithTimestamp:self.storyTimestamp.unsignedLongLongValue + author:self.storyAuthorAddress + transaction:transaction]; + if (storyMessage) { + [self.databaseStorage touchStoryMessage:storyMessage transaction:transaction]; + } } - (void)removeAllAttachmentsWithTransaction:(SDSAnyWriteTransaction *)transaction @@ -780,11 +826,22 @@ static const NSUInteger OWSMessageSchemaVersion = 4; - (BOOL)hasRenderableContent { + // Story replies currently only support a subset of message features, so may not + // be renderable in some circumstances where a normal message would be. + if (self.isStoryReply) { + return [self hasRenderableStoryReplyContent]; + } + // We DO NOT consider a message with just a linkPreview // or quotedMessage to be renderable. return (self.body.length > 0 || self.attachmentIds.count > 0 || self.contactShare != nil || self.messageSticker); } +- (BOOL)hasRenderableStoryReplyContent +{ + return self.body.length > 0 || self.storyReactionEmoji.isSingleEmoji; +} + #pragma mark - View Once - (void)updateWithViewOnceCompleteAndRemoveRenderableContentWithTransaction:(SDSAnyWriteTransaction *)transaction @@ -835,6 +892,7 @@ static const NSUInteger OWSMessageSchemaVersion = 4; message.linkPreview = nil; message.messageSticker = nil; message.attachmentIds = @[]; + message.storyReactionEmoji = nil; OWSAssertDebug(!message.hasRenderableContent); messageUpdateBlock(message); diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage+SDS.swift b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage+SDS.swift index 142ba53976..225ff49178 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage+SDS.swift @@ -125,7 +125,11 @@ class TSOutgoingMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h index 3cc56f2ffe..3060316d5b 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h @@ -131,12 +131,16 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) { expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage @@ -149,7 +153,7 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) { mostRecentFailureText:(nullable NSString *)mostRecentFailureText recipientAddressStates:(nullable NSDictionary *)recipientAddressStates storedMessageState:(TSOutgoingMessageState)storedMessageState -NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:wasRemotelyDeleted:customMessage:groupMetaMessage:hasLegacyMessageState:hasSyncedTranscript:isFromLinkedDevice:isVoiceMessage:legacyMessageState:legacyWasDelivered:mostRecentFailureText:recipientAddressStates:storedMessageState:)); +NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isGroupStoryReply:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:storyAuthorUuidString:storyReactionEmoji:storyTimestamp:wasRemotelyDeleted:customMessage:groupMetaMessage:hasLegacyMessageState:hasSyncedTranscript:isFromLinkedDevice:isVoiceMessage:legacyMessageState:legacyWasDelivered:mostRecentFailureText:recipientAddressStates:storedMessageState:)); // clang-format on diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m index b03a73e354..6f7992abb0 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m @@ -135,12 +135,16 @@ NSUInteger const TSOutgoingMessageSchemaVersion = 1; expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage @@ -167,12 +171,16 @@ NSUInteger const TSOutgoingMessageSchemaVersion = 1; expireStartedAt:expireStartedAt expiresAt:expiresAt expiresInSeconds:expiresInSeconds + isGroupStoryReply:isGroupStoryReply isViewOnceComplete:isViewOnceComplete isViewOnceMessage:isViewOnceMessage linkPreview:linkPreview messageSticker:messageSticker quotedMessage:quotedMessage storedShouldStartExpireTimer:storedShouldStartExpireTimer + storyAuthorUuidString:storyAuthorUuidString + storyReactionEmoji:storyReactionEmoji + storyTimestamp:storyTimestamp wasRemotelyDeleted:wasRemotelyDeleted]; if (!self) { @@ -1083,6 +1091,40 @@ NSUInteger const TSOutgoingMessageSchemaVersion = 1; } } + // Story Context + if (self.storyTimestamp && self.storyAuthorUuidString) { + if (self.storyReactionEmoji) { + SSKProtoDataMessageReactionBuilder *reactionBuilder = + [SSKProtoDataMessageReaction builderWithEmoji:self.storyReactionEmoji + timestamp:self.storyTimestamp.longLongValue]; + [reactionBuilder setAuthorUuid:self.storyAuthorUuidString]; + + NSError *error; + SSKProtoDataMessageReaction *_Nullable reaction = [reactionBuilder buildAndReturnError:&error]; + if (error || !reaction) { + OWSFailDebug(@"Could not build story reaction protobuf: %@.", error); + } else { + [builder setReaction:reaction]; + + if (requiredProtocolVersion < SSKProtoDataMessageProtocolVersionReactions) { + requiredProtocolVersion = SSKProtoDataMessageProtocolVersionReactions; + } + } + } + + SSKProtoDataMessageStoryContextBuilder *storyContextBuilder = [SSKProtoDataMessageStoryContext builder]; + [storyContextBuilder setAuthorUuid:self.storyAuthorUuidString]; + [storyContextBuilder setSentTimestamp:self.storyTimestamp.longLongValue]; + + NSError *error; + SSKProtoDataMessageStoryContext *_Nullable storyContext = [storyContextBuilder buildAndReturnError:&error]; + if (error || !storyContext) { + OWSFailDebug(@"Could not build storyContext protobuf: %@.", error); + } else { + [builder setStoryContext:storyContext]; + } + } + [builder setExpireTimer:self.expiresInSeconds]; // Group Messages diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.swift b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.swift index 86adac63fb..a9ff74a6e6 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.swift @@ -36,7 +36,10 @@ public class TSOutgoingMessageBuilder: TSMessageBuilder { messageSticker: MessageSticker? = nil, isViewOnceMessage: Bool = false, changeActionsProtoData: Data? = nil, - additionalRecipients: [SignalServiceAddress]? = nil) { + additionalRecipients: [SignalServiceAddress]? = nil, + storyAuthorAddress: SignalServiceAddress? = nil, + storyTimestamp: UInt64? = nil, + storyReactionEmoji: String? = nil) { super.init(thread: thread, timestamp: timestamp, @@ -49,7 +52,10 @@ public class TSOutgoingMessageBuilder: TSMessageBuilder { contactShare: contactShare, linkPreview: linkPreview, messageSticker: messageSticker, - isViewOnceMessage: isViewOnceMessage) + isViewOnceMessage: isViewOnceMessage, + storyAuthorAddress: storyAuthorAddress, + storyTimestamp: storyTimestamp, + storyReactionEmoji: storyReactionEmoji) self.isVoiceMessage = isVoiceMessage self.groupMetaMessage = groupMetaMessage @@ -88,7 +94,10 @@ public class TSOutgoingMessageBuilder: TSMessageBuilder { messageSticker: MessageSticker?, isViewOnceMessage: Bool, changeActionsProtoData: Data?, - additionalRecipients: [SignalServiceAddress]?) -> TSOutgoingMessageBuilder { + additionalRecipients: [SignalServiceAddress]?, + storyAuthorAddress: SignalServiceAddress?, + storyTimestamp: NSNumber?, + storyReactionEmoji: String?) -> TSOutgoingMessageBuilder { return TSOutgoingMessageBuilder(thread: thread, timestamp: timestamp, messageBody: messageBody, @@ -104,7 +113,10 @@ public class TSOutgoingMessageBuilder: TSMessageBuilder { messageSticker: messageSticker, isViewOnceMessage: isViewOnceMessage, changeActionsProtoData: changeActionsProtoData, - additionalRecipients: additionalRecipients) + additionalRecipients: additionalRecipients, + storyAuthorAddress: storyAuthorAddress, + storyTimestamp: storyTimestamp?.uint64Value, + storyReactionEmoji: storyReactionEmoji) } private var hasBuilt = false diff --git a/SignalServiceKit/src/Messages/Interactions/TSUnreadIndicatorInteraction+SDS.swift b/SignalServiceKit/src/Messages/Interactions/TSUnreadIndicatorInteraction+SDS.swift index 65b2264a7a..049142dc90 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSUnreadIndicatorInteraction+SDS.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSUnreadIndicatorInteraction+SDS.swift @@ -125,7 +125,11 @@ class TSUnreadIndicatorInteractionSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = nil + let storyTimestamp: UInt64? = nil + let isGroupStoryReply: Bool? = nil + let storyReactionEmoji: String? = nil - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyErrorMessage+SDS.swift b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyErrorMessage+SDS.swift index 2075833973..20d441f010 100644 --- a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyErrorMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyErrorMessage+SDS.swift @@ -125,7 +125,11 @@ class TSInvalidIdentityKeyErrorMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyReceivingErrorMessage+SDS.swift b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyReceivingErrorMessage+SDS.swift index c5f0b56caf..7349681cf0 100644 --- a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyReceivingErrorMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyReceivingErrorMessage+SDS.swift @@ -125,7 +125,11 @@ class TSInvalidIdentityKeyReceivingErrorMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyReceivingErrorMessage.h b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyReceivingErrorMessage.h index 9986f0874c..7a67caaad8 100644 --- a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyReceivingErrorMessage.h +++ b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyReceivingErrorMessage.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import @@ -38,12 +38,16 @@ __attribute__((deprecated)) @interface TSInvalidIdentityKeyReceivingErrorMessage expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted errorType:(TSErrorMessageType)errorType read:(BOOL)read @@ -52,7 +56,7 @@ __attribute__((deprecated)) @interface TSInvalidIdentityKeyReceivingErrorMessage wasIdentityVerified:(BOOL)wasIdentityVerified authorId:(NSString *)authorId envelopeData:(nullable NSData *)envelopeData -NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:wasRemotelyDeleted:errorType:read:recipientAddress:sender:wasIdentityVerified:authorId:envelopeData:)); +NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isGroupStoryReply:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:storyAuthorUuidString:storyReactionEmoji:storyTimestamp:wasRemotelyDeleted:errorType:read:recipientAddress:sender:wasIdentityVerified:authorId:envelopeData:)); // clang-format on diff --git a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyReceivingErrorMessage.m b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyReceivingErrorMessage.m index 2c9ab25b9a..33db1b1873 100644 --- a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyReceivingErrorMessage.m +++ b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeyReceivingErrorMessage.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import "TSInvalidIdentityKeyReceivingErrorMessage.h" @@ -99,12 +99,16 @@ __attribute__((deprecated)) @interface TSInvalidIdentityKeyReceivingErrorMessage expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted errorType:(TSErrorMessageType)errorType read:(BOOL)read @@ -127,12 +131,16 @@ __attribute__((deprecated)) @interface TSInvalidIdentityKeyReceivingErrorMessage expireStartedAt:expireStartedAt expiresAt:expiresAt expiresInSeconds:expiresInSeconds + isGroupStoryReply:isGroupStoryReply isViewOnceComplete:isViewOnceComplete isViewOnceMessage:isViewOnceMessage linkPreview:linkPreview messageSticker:messageSticker quotedMessage:quotedMessage storedShouldStartExpireTimer:storedShouldStartExpireTimer + storyAuthorUuidString:storyAuthorUuidString + storyReactionEmoji:storyReactionEmoji + storyTimestamp:storyTimestamp wasRemotelyDeleted:wasRemotelyDeleted errorType:errorType read:read diff --git a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeySendingErrorMessage+SDS.swift b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeySendingErrorMessage+SDS.swift index 32ac9ab5f4..92ec18130f 100644 --- a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeySendingErrorMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeySendingErrorMessage+SDS.swift @@ -125,7 +125,11 @@ class TSInvalidIdentityKeySendingErrorMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeySendingErrorMessage.h b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeySendingErrorMessage.h index effae9d7c9..ed3c03363a 100644 --- a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeySendingErrorMessage.h +++ b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeySendingErrorMessage.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import @@ -37,12 +37,16 @@ __attribute__((deprecated)) @interface TSInvalidIdentityKeySendingErrorMessage : expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted errorType:(TSErrorMessageType)errorType read:(BOOL)read @@ -51,7 +55,7 @@ __attribute__((deprecated)) @interface TSInvalidIdentityKeySendingErrorMessage : wasIdentityVerified:(BOOL)wasIdentityVerified messageId:(NSString *)messageId preKeyBundle:(PreKeyBundle *)preKeyBundle -NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:wasRemotelyDeleted:errorType:read:recipientAddress:sender:wasIdentityVerified:messageId:preKeyBundle:)); +NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isGroupStoryReply:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:storyAuthorUuidString:storyReactionEmoji:storyTimestamp:wasRemotelyDeleted:errorType:read:recipientAddress:sender:wasIdentityVerified:messageId:preKeyBundle:)); // clang-format on diff --git a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeySendingErrorMessage.m b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeySendingErrorMessage.m index 00a66b3ef7..27aa0743b7 100644 --- a/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeySendingErrorMessage.m +++ b/SignalServiceKit/src/Messages/InvalidKeyMessages/TSInvalidIdentityKeySendingErrorMessage.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import "TSInvalidIdentityKeySendingErrorMessage.h" @@ -52,12 +52,16 @@ NS_ASSUME_NONNULL_BEGIN expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted errorType:(TSErrorMessageType)errorType read:(BOOL)read @@ -80,12 +84,16 @@ NS_ASSUME_NONNULL_BEGIN expireStartedAt:expireStartedAt expiresAt:expiresAt expiresInSeconds:expiresInSeconds + isGroupStoryReply:isGroupStoryReply isViewOnceComplete:isViewOnceComplete isViewOnceMessage:isViewOnceMessage linkPreview:linkPreview messageSticker:messageSticker quotedMessage:quotedMessage storedShouldStartExpireTimer:storedShouldStartExpireTimer + storyAuthorUuidString:storyAuthorUuidString + storyReactionEmoji:storyReactionEmoji + storyTimestamp:storyTimestamp wasRemotelyDeleted:wasRemotelyDeleted errorType:errorType read:read diff --git a/SignalServiceKit/src/Messages/OWSAddToContactsOfferMessage+SDS.swift b/SignalServiceKit/src/Messages/OWSAddToContactsOfferMessage+SDS.swift index 168ca7366f..fc6c562958 100644 --- a/SignalServiceKit/src/Messages/OWSAddToContactsOfferMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/OWSAddToContactsOfferMessage+SDS.swift @@ -125,7 +125,11 @@ class OWSAddToContactsOfferMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/OWSAddToProfileWhitelistOfferMessage+SDS.swift b/SignalServiceKit/src/Messages/OWSAddToProfileWhitelistOfferMessage+SDS.swift index 9a58a4abdb..4ffcb5fad4 100644 --- a/SignalServiceKit/src/Messages/OWSAddToProfileWhitelistOfferMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/OWSAddToProfileWhitelistOfferMessage+SDS.swift @@ -125,7 +125,11 @@ class OWSAddToProfileWhitelistOfferMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/OWSGroupCallMessage+SDS.swift b/SignalServiceKit/src/Messages/OWSGroupCallMessage+SDS.swift index 3525e0fd18..bf5dc3f020 100644 --- a/SignalServiceKit/src/Messages/OWSGroupCallMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/OWSGroupCallMessage+SDS.swift @@ -125,7 +125,11 @@ class OWSGroupCallMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = nil + let storyTimestamp: UInt64? = nil + let isGroupStoryReply: Bool? = nil + let storyReactionEmoji: String? = nil - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m index d5fe7b2fcf..aba8b94d4d 100644 --- a/SignalServiceKit/src/Messages/OWSMessageManager.m +++ b/SignalServiceKit/src/Messages/OWSMessageManager.m @@ -577,7 +577,7 @@ NS_ASSUME_NONNULL_BEGIN } } - if (dataMessage.storyContext != nil) { + if (!SSKFeatureFlags.stories && dataMessage.storyContext != nil) { OWSLogInfo(@"Ignoring message (author: %@, timestamp: %llu) related to story (author: %@, timestamp: %llu)", envelope.sourceAddress, dataMessage.timestamp, @@ -1605,9 +1605,10 @@ NS_ASSUME_NONNULL_BEGIN return; } OWSReactionProcessingResult result = [OWSReactionManager processIncomingReaction:dataMessage.reaction - threadId:thread.uniqueId + thread:thread reactor:envelope.sourceAddress timestamp:syncMessage.sent.timestamp + serverTimestamp:envelope.serverTimestamp transaction:transaction]; switch (result) { case OWSReactionProcessingResultSuccess: @@ -1956,9 +1957,10 @@ NS_ASSUME_NONNULL_BEGIN OWSLogInfo(@"Reaction: %@", messageDescription); } OWSReactionProcessingResult result = [OWSReactionManager processIncomingReaction:dataMessage.reaction - threadId:thread.uniqueId + thread:thread reactor:envelope.sourceAddress timestamp:timestamp + serverTimestamp:envelope.serverTimestamp transaction:transaction]; switch (result) { @@ -2105,6 +2107,25 @@ NS_ASSUME_NONNULL_BEGIN thread:thread transaction:transaction]; + NSNumber *_Nullable storyTimestamp; + SignalServiceAddress *_Nullable storyAuthorAddress; + if (dataMessage.storyContext != nil && dataMessage.storyContext.hasSentTimestamp + && dataMessage.storyContext.hasAuthorUuid) { + OWSLogInfo( + @"Processing storyContext for message with timestamp: %llu, storyTimestamp: %llu, and author uuid: %@", + envelope.timestamp, + dataMessage.storyContext.sentTimestamp, + dataMessage.storyContext.authorUuid); + + storyTimestamp = @(dataMessage.storyContext.sentTimestamp); + storyAuthorAddress = [[SignalServiceAddress alloc] initWithUuidString:dataMessage.storyContext.authorUuid]; + + if (!storyAuthorAddress.isValid) { + OWSFailDebug(@"Discarding story reply with invalid address %@", storyAuthorAddress); + return nil; + } + } + // Legit usage of senderTimestamp when creating an incoming group message record // // The builder() factory method requires us to specify every @@ -2126,7 +2147,10 @@ NS_ASSUME_NONNULL_BEGIN serverDeliveryTimestamp:serverDeliveryTimestamp serverGuid:serverGuid wasReceivedByUD:wasReceivedByUD - isViewOnceMessage:isViewOnceMessage]; + isViewOnceMessage:isViewOnceMessage + storyAuthorAddress:storyAuthorAddress + storyTimestamp:storyTimestamp + storyReactionEmoji:nil]; TSIncomingMessage *message = [incomingMessageBuilder build]; if (!message) { OWSFailDebug(@"Missing incomingMessage."); @@ -2139,8 +2163,8 @@ NS_ASSUME_NONNULL_BEGIN // Typically `hasRenderableContent` will depend on whether or not the message has any attachmentIds // But since the message is partially built and doesn't have the attachments yet, we check - // for attachments explicitly. - if (!message.hasRenderableContent && dataMessage.attachments.count == 0) { + // for attachments explicitly. Story replies cannot have attachments, so we can bail on them here immediately. + if (!message.hasRenderableContent && (dataMessage.attachments.count == 0 || message.isStoryReply)) { OWSLogWarn(@"Ignoring empty: %@", messageDescription); OWSLogVerbose(@"Ignoring empty message(envelope): %@", envelope.debugDescription); OWSLogVerbose(@"Ignoring empty message(dataMessage): %@", dataMessage.debugDescription); diff --git a/SignalServiceKit/src/Messages/OWSRecoverableDecryptionPlaceholder+SDS.swift b/SignalServiceKit/src/Messages/OWSRecoverableDecryptionPlaceholder+SDS.swift index 0bf2e103fb..16f0a40b3f 100644 --- a/SignalServiceKit/src/Messages/OWSRecoverableDecryptionPlaceholder+SDS.swift +++ b/SignalServiceKit/src/Messages/OWSRecoverableDecryptionPlaceholder+SDS.swift @@ -125,7 +125,11 @@ class OWSRecoverableDecryptionPlaceholderSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/OWSUnknownContactBlockOfferMessage+SDS.swift b/SignalServiceKit/src/Messages/OWSUnknownContactBlockOfferMessage+SDS.swift index c70e26331e..0455a2f5b9 100644 --- a/SignalServiceKit/src/Messages/OWSUnknownContactBlockOfferMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/OWSUnknownContactBlockOfferMessage+SDS.swift @@ -125,7 +125,11 @@ class OWSUnknownContactBlockOfferMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/OWSUnknownProtocolVersionMessage+SDS.swift b/SignalServiceKit/src/Messages/OWSUnknownProtocolVersionMessage+SDS.swift index 50271fdf7c..4dd5a8acb6 100644 --- a/SignalServiceKit/src/Messages/OWSUnknownProtocolVersionMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/OWSUnknownProtocolVersionMessage+SDS.swift @@ -125,7 +125,11 @@ class OWSUnknownProtocolVersionMessageSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/OWSUnknownProtocolVersionMessage.h b/SignalServiceKit/src/Messages/OWSUnknownProtocolVersionMessage.h index 6cea14ee5d..e3eaf9aab0 100644 --- a/SignalServiceKit/src/Messages/OWSUnknownProtocolVersionMessage.h +++ b/SignalServiceKit/src/Messages/OWSUnknownProtocolVersionMessage.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import @@ -66,12 +66,16 @@ NS_ASSUME_NONNULL_BEGIN expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage infoMessageUserInfo:(nullable NSDictionary *)infoMessageUserInfo @@ -80,7 +84,7 @@ NS_ASSUME_NONNULL_BEGIN unregisteredAddress:(nullable SignalServiceAddress *)unregisteredAddress protocolVersion:(NSUInteger)protocolVersion sender:(nullable SignalServiceAddress *)sender -NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:wasRemotelyDeleted:customMessage:infoMessageUserInfo:messageType:read:unregisteredAddress:protocolVersion:sender:)); +NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isGroupStoryReply:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:storyAuthorUuidString:storyReactionEmoji:storyTimestamp:wasRemotelyDeleted:customMessage:infoMessageUserInfo:messageType:read:unregisteredAddress:protocolVersion:sender:)); // clang-format on diff --git a/SignalServiceKit/src/Messages/OWSUnknownProtocolVersionMessage.m b/SignalServiceKit/src/Messages/OWSUnknownProtocolVersionMessage.m index a02c70715e..e905b37722 100644 --- a/SignalServiceKit/src/Messages/OWSUnknownProtocolVersionMessage.m +++ b/SignalServiceKit/src/Messages/OWSUnknownProtocolVersionMessage.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import "OWSUnknownProtocolVersionMessage.h" @@ -78,12 +78,16 @@ NSUInteger const OWSUnknownProtocolVersionMessageSchemaVersion = 1; expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage infoMessageUserInfo:(nullable NSDictionary *)infoMessageUserInfo @@ -106,12 +110,16 @@ NSUInteger const OWSUnknownProtocolVersionMessageSchemaVersion = 1; expireStartedAt:expireStartedAt expiresAt:expiresAt expiresInSeconds:expiresInSeconds + isGroupStoryReply:isGroupStoryReply isViewOnceComplete:isViewOnceComplete isViewOnceMessage:isViewOnceMessage linkPreview:linkPreview messageSticker:messageSticker quotedMessage:quotedMessage storedShouldStartExpireTimer:storedShouldStartExpireTimer + storyAuthorUuidString:storyAuthorUuidString + storyReactionEmoji:storyReactionEmoji + storyTimestamp:storyTimestamp wasRemotelyDeleted:wasRemotelyDeleted customMessage:customMessage infoMessageUserInfo:infoMessageUserInfo diff --git a/SignalServiceKit/src/Messages/Payments/OWSOutgoingPaymentMessage+SDS.swift b/SignalServiceKit/src/Messages/Payments/OWSOutgoingPaymentMessage+SDS.swift index 0058e550ca..e4218e52cf 100644 --- a/SignalServiceKit/src/Messages/Payments/OWSOutgoingPaymentMessage+SDS.swift +++ b/SignalServiceKit/src/Messages/Payments/OWSOutgoingPaymentMessage+SDS.swift @@ -125,7 +125,11 @@ class OWSOutgoingPaymentMessageSerializer: SDSSerializer { let paymentRequest: Data? = optionalArchive(model.paymentRequest) let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = model.storyAuthorUuidString + let storyTimestamp: UInt64? = archiveOptionalNSNumber(model.storyTimestamp, conversion: { $0.uint64Value }) + let isGroupStoryReply: Bool? = model.isGroupStoryReply + let storyReactionEmoji: String? = model.storyReactionEmoji - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Messages/Payments/OWSOutgoingPaymentMessage.h b/SignalServiceKit/src/Messages/Payments/OWSOutgoingPaymentMessage.h index bb000b8bcc..cdc3cddb3d 100644 --- a/SignalServiceKit/src/Messages/Payments/OWSOutgoingPaymentMessage.h +++ b/SignalServiceKit/src/Messages/Payments/OWSOutgoingPaymentMessage.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import @@ -43,12 +43,16 @@ NS_ASSUME_NONNULL_BEGIN expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage @@ -64,7 +68,7 @@ NS_ASSUME_NONNULL_BEGIN paymentCancellation:(nullable TSPaymentCancellation *)paymentCancellation paymentNotification:(nullable TSPaymentNotification *)paymentNotification paymentRequest:(nullable TSPaymentRequest *)paymentRequest -NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:wasRemotelyDeleted:customMessage:groupMetaMessage:hasLegacyMessageState:hasSyncedTranscript:isFromLinkedDevice:isVoiceMessage:legacyMessageState:legacyWasDelivered:mostRecentFailureText:recipientAddressStates:storedMessageState:paymentCancellation:paymentNotification:paymentRequest:)); +NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp:sortId:timestamp:uniqueThreadId:attachmentIds:body:bodyRanges:contactShare:expireStartedAt:expiresAt:expiresInSeconds:isGroupStoryReply:isViewOnceComplete:isViewOnceMessage:linkPreview:messageSticker:quotedMessage:storedShouldStartExpireTimer:storyAuthorUuidString:storyReactionEmoji:storyTimestamp:wasRemotelyDeleted:customMessage:groupMetaMessage:hasLegacyMessageState:hasSyncedTranscript:isFromLinkedDevice:isVoiceMessage:legacyMessageState:legacyWasDelivered:mostRecentFailureText:recipientAddressStates:storedMessageState:paymentCancellation:paymentNotification:paymentRequest:)); // clang-format on diff --git a/SignalServiceKit/src/Messages/Payments/OWSOutgoingPaymentMessage.m b/SignalServiceKit/src/Messages/Payments/OWSOutgoingPaymentMessage.m index 9fd5a1ecea..5966cc3775 100644 --- a/SignalServiceKit/src/Messages/Payments/OWSOutgoingPaymentMessage.m +++ b/SignalServiceKit/src/Messages/Payments/OWSOutgoingPaymentMessage.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import "OWSOutgoingPaymentMessage.h" @@ -57,12 +57,16 @@ NS_ASSUME_NONNULL_BEGIN expireStartedAt:(uint64_t)expireStartedAt expiresAt:(uint64_t)expiresAt expiresInSeconds:(unsigned int)expiresInSeconds + isGroupStoryReply:(BOOL)isGroupStoryReply isViewOnceComplete:(BOOL)isViewOnceComplete isViewOnceMessage:(BOOL)isViewOnceMessage linkPreview:(nullable OWSLinkPreview *)linkPreview messageSticker:(nullable MessageSticker *)messageSticker quotedMessage:(nullable TSQuotedMessage *)quotedMessage storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer + storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString + storyReactionEmoji:(nullable NSString *)storyReactionEmoji + storyTimestamp:(nullable NSNumber *)storyTimestamp wasRemotelyDeleted:(BOOL)wasRemotelyDeleted customMessage:(nullable NSString *)customMessage groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage @@ -92,12 +96,16 @@ NS_ASSUME_NONNULL_BEGIN expireStartedAt:expireStartedAt expiresAt:expiresAt expiresInSeconds:expiresInSeconds + isGroupStoryReply:isGroupStoryReply isViewOnceComplete:isViewOnceComplete isViewOnceMessage:isViewOnceMessage linkPreview:linkPreview messageSticker:messageSticker quotedMessage:quotedMessage storedShouldStartExpireTimer:storedShouldStartExpireTimer + storyAuthorUuidString:storyAuthorUuidString + storyReactionEmoji:storyReactionEmoji + storyTimestamp:storyTimestamp wasRemotelyDeleted:wasRemotelyDeleted customMessage:customMessage groupMetaMessage:groupMetaMessage diff --git a/SignalServiceKit/src/Messages/Reactions/ReactionManager.swift b/SignalServiceKit/src/Messages/Reactions/ReactionManager.swift index 65822f806d..d9af0281ae 100644 --- a/SignalServiceKit/src/Messages/Reactions/ReactionManager.swift +++ b/SignalServiceKit/src/Messages/Reactions/ReactionManager.swift @@ -123,9 +123,10 @@ public class ReactionManager: NSObject { @objc public class func processIncomingReaction( _ reaction: SSKProtoDataMessageReaction, - threadId: String, + thread: TSThread, reactor: SignalServiceAddress, timestamp: UInt64, + serverTimestamp: UInt64, transaction: SDSAnyWriteTransaction ) -> ReactionProcessingResult { guard let emoji = reaction.emoji.strippedOrNil else { @@ -146,50 +147,78 @@ public class ReactionManager: NSObject { return .invalidReaction } - guard let message = InteractionFinder.findMessage( + if let message = InteractionFinder.findMessage( withTimestamp: reaction.timestamp, - threadId: threadId, + threadId: thread.uniqueId, author: messageAuthor, transaction: transaction - ) else { + ) { + guard !message.wasRemotelyDeleted else { + Logger.info("Ignoring reaction for a message that was remotely deleted") + return .invalidReaction + } + + // If this is a reaction removal, we want to remove *any* reaction from this author + // on this message, regardless of the specified emoji. + if reaction.hasRemove, reaction.remove { + message.removeReaction(for: reactor, transaction: transaction) + } else { + let reaction = message.recordReaction( + for: reactor, + emoji: emoji, + sentAtTimestamp: timestamp, + receivedAtTimestamp: NSDate.ows_millisecondTimeStamp(), + transaction: transaction + ) + + // If this is a reaction to a message we sent, notify the user. + if let reaction = reaction, let message = message as? TSOutgoingMessage, !reactor.isLocalAddress { + self.notificationsManager?.notifyUser(forReaction: reaction, + onOutgoingMessage: message, + thread: thread, + transaction: transaction) + } + } + + return .success + } else if let storyMessage = StoryFinder.story( + timestamp: reaction.timestamp, + author: messageAuthor, + transaction: transaction + ) { + // Reaction to stories show up as normal messages, they + // are not associated with standard interactions. As such + // we need to insert an incoming/outgoing message as appropriate. + + func populateStoryContext(on builder: TSMessageBuilder) { + builder.timestamp = timestamp + builder.storyReactionEmoji = reaction.emoji + builder.storyTimestamp = NSNumber(value: storyMessage.timestamp) + builder.storyAuthorAddress = storyMessage.authorAddress + } + + let message: TSMessage + + if reactor.isLocalAddress { + let builder = TSOutgoingMessageBuilder(thread: thread) + populateStoryContext(on: builder) + message = builder.build() + } else { + let builder = TSIncomingMessageBuilder(thread: thread) + builder.authorAddress = reactor + builder.serverTimestamp = NSNumber(value: serverTimestamp) + populateStoryContext(on: builder) + message = builder.build() + } + + message.anyInsert(transaction: transaction) + + return .success + } else { // This is potentially normal. For example, we could've deleted the message locally. Logger.info("Received reaction for a message that doesn't exist \(timestamp)") return .associatedMessageMissing } - - guard !message.wasRemotelyDeleted else { - Logger.info("Ignoring reaction for a message that was remotely deleted") - return .invalidReaction - } - - // If this is a reaction removal, we want to remove *any* reaction from this author - // on this message, regardless of the specified emoji. - if reaction.hasRemove, reaction.remove { - message.removeReaction(for: reactor, transaction: transaction) - } else { - let reaction = message.recordReaction( - for: reactor, - emoji: emoji, - sentAtTimestamp: timestamp, - receivedAtTimestamp: NSDate.ows_millisecondTimeStamp(), - transaction: transaction - ) - - // If this is a reaction to a message we sent, notify the user. - if let reaction = reaction, let message = message as? TSOutgoingMessage, !reactor.isLocalAddress { - guard let thread = TSThread.anyFetch(uniqueId: threadId, transaction: transaction) else { - owsFailDebug("Failed to lookup thread for reaction notification.") - return .success - } - - self.notificationsManager?.notifyUser(forReaction: reaction, - onOutgoingMessage: message, - thread: thread, - transaction: transaction) - } - } - - return .success } @objc diff --git a/SignalServiceKit/src/Messages/Stories/StoryMessage.swift b/SignalServiceKit/src/Messages/Stories/StoryMessage.swift index 7d8f327ef0..78795a5c7a 100644 --- a/SignalServiceKit/src/Messages/Stories/StoryMessage.swift +++ b/SignalServiceKit/src/Messages/Stories/StoryMessage.swift @@ -162,7 +162,19 @@ public final class StoryMessage: NSObject, SDSCodableModel { // MARK: - public func anyDidRemove(transaction: SDSAnyWriteTransaction) { - // TODO: Cleanup associated records + // Delete all group replies for the message. + InteractionFinder.enumerateGroupReplies(for: self, transaction: transaction) { reply, _ in + reply.anyRemove(transaction: transaction) + } + + // Delete all attachments for the message. + for id in allAttachmentIds { + guard let attachment = TSAttachment.anyFetch(uniqueId: id, transaction: transaction) else { + owsFailDebug("Missing attachment for StoryMessage \(id)") + continue + } + attachment.anyRemove(transaction: transaction) + } } @objc diff --git a/SignalServiceKit/src/Messages/TSCall+SDS.swift b/SignalServiceKit/src/Messages/TSCall+SDS.swift index 334cfc0cd8..8294cad37e 100644 --- a/SignalServiceKit/src/Messages/TSCall+SDS.swift +++ b/SignalServiceKit/src/Messages/TSCall+SDS.swift @@ -125,7 +125,11 @@ class TSCallSerializer: SDSSerializer { let paymentRequest: Data? = nil let viewed: Bool? = nil let serverGuid: String? = nil + let storyAuthorUuidString: String? = nil + let storyTimestamp: UInt64? = nil + let isGroupStoryReply: Bool? = nil + let storyReactionEmoji: String? = nil - return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid) + return InteractionRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, receivedAtTimestamp: receivedAtTimestamp, timestamp: timestamp, threadUniqueId: threadUniqueId, attachmentIds: attachmentIds, authorId: authorId, authorPhoneNumber: authorPhoneNumber, authorUUID: authorUUID, body: body, callType: callType, configurationDurationSeconds: configurationDurationSeconds, configurationIsEnabled: configurationIsEnabled, contactShare: contactShare, createdByRemoteName: createdByRemoteName, createdInExistingGroup: createdInExistingGroup, customMessage: customMessage, envelopeData: envelopeData, errorType: errorType, expireStartedAt: expireStartedAt, expiresAt: expiresAt, expiresInSeconds: expiresInSeconds, groupMetaMessage: groupMetaMessage, hasLegacyMessageState: hasLegacyMessageState, hasSyncedTranscript: hasSyncedTranscript, isFromLinkedDevice: isFromLinkedDevice, isLocalChange: isLocalChange, isViewOnceComplete: isViewOnceComplete, isViewOnceMessage: isViewOnceMessage, isVoiceMessage: isVoiceMessage, legacyMessageState: legacyMessageState, legacyWasDelivered: legacyWasDelivered, linkPreview: linkPreview, messageId: messageId, messageSticker: messageSticker, messageType: messageType, mostRecentFailureText: mostRecentFailureText, preKeyBundle: preKeyBundle, protocolVersion: protocolVersion, quotedMessage: quotedMessage, read: read, recipientAddress: recipientAddress, recipientAddressStates: recipientAddressStates, sender: sender, serverTimestamp: serverTimestamp, sourceDeviceId: sourceDeviceId, storedMessageState: storedMessageState, storedShouldStartExpireTimer: storedShouldStartExpireTimer, unregisteredAddress: unregisteredAddress, verificationState: verificationState, wasReceivedByUD: wasReceivedByUD, infoMessageUserInfo: infoMessageUserInfo, wasRemotelyDeleted: wasRemotelyDeleted, bodyRanges: bodyRanges, offerType: offerType, serverDeliveryTimestamp: serverDeliveryTimestamp, eraId: eraId, hasEnded: hasEnded, creatorUuid: creatorUuid, joinedMemberUuids: joinedMemberUuids, wasIdentityVerified: wasIdentityVerified, paymentCancellation: paymentCancellation, paymentNotification: paymentNotification, paymentRequest: paymentRequest, viewed: viewed, serverGuid: serverGuid, storyAuthorUuidString: storyAuthorUuidString, storyTimestamp: storyTimestamp, isGroupStoryReply: isGroupStoryReply, storyReactionEmoji: storyReactionEmoji) } } diff --git a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m index 73f7bba433..f30a8013af 100644 --- a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m +++ b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m @@ -487,6 +487,10 @@ NSString *const OWSRequestKey_AuthKey = @"AuthKey"; capabilities[@"gv1-migration"] = @(YES); capabilities[@"senderKey"] = @(YES); + if (SSKFeatureFlags.stories) { + capabilities[@"stories"] = @(YES); + } + // If the storage service requires (or will require) secondary devices // to have a capability in order to be linked, we might need to always // set that capability here if isSecondaryDevice is true. diff --git a/SignalServiceKit/src/Storage/Database/GRDBSchemaMigrator.swift b/SignalServiceKit/src/Storage/Database/GRDBSchemaMigrator.swift index c0c6716c07..60b7432444 100644 --- a/SignalServiceKit/src/Storage/Database/GRDBSchemaMigrator.swift +++ b/SignalServiceKit/src/Storage/Database/GRDBSchemaMigrator.swift @@ -124,6 +124,7 @@ public class GRDBSchemaMigrator: NSObject { case createSubscriptionDurableJob case addReceiptPresentationToSubscriptionDurableJob case createStoryMessageTable + case addColumnsForStoryContext // NOTE: Every time we add a migration id, consider // incrementing grdbSchemaVersionLatest. @@ -166,7 +167,7 @@ public class GRDBSchemaMigrator: NSObject { } public static let grdbSchemaVersionDefault: UInt = 0 - public static let grdbSchemaVersionLatest: UInt = 33 + public static let grdbSchemaVersionLatest: UInt = 34 // An optimization for new users, we have the first migration import the latest schema // and mark any other migrations as "already run". @@ -1565,6 +1566,38 @@ public class GRDBSchemaMigrator: NSObject { } } + migrator.registerMigration(MigrationId.addColumnsForStoryContext.rawValue) { db in + do { + try db.alter(table: "model_TSInteraction") { (table: TableAlteration) -> Void in + table.add(column: "storyAuthorUuidString", .text) + table.add(column: "storyTimestamp", .integer) + table.add(column: "isGroupStoryReply", .boolean) + table.add(column: "storyReactionEmoji", .text) + } + + try db.execute(sql: "UPDATE model_TSInteraction SET isGroupStoryReply = 0") + + try db.execute(sql: """ + DROP INDEX index_model_TSInteraction_ConversationLoadInteractionCount; + DROP INDEX index_model_TSInteraction_ConversationLoadInteractionDistance; + DROP INDEX index_interactions_unread_counts; + + CREATE INDEX index_model_TSInteraction_ConversationLoadInteractionCount + ON model_TSInteraction(uniqueThreadId, isGroupStoryReply, recordType) + WHERE recordType IS NOT \(SDSRecordType.recoverableDecryptionPlaceholder.rawValue); + + CREATE INDEX index_model_TSInteraction_ConversationLoadInteractionDistance + ON model_TSInteraction(uniqueThreadId, id, isGroupStoryReply, recordType, uniqueId) + WHERE recordType IS NOT \(SDSRecordType.recoverableDecryptionPlaceholder.rawValue); + + CREATE INDEX index_model_TSInteraction_UnreadCount + ON model_TSInteraction(read, isGroupStoryReply, uniqueThreadId, recordType); + """) + } catch { + owsFail("Error: \(error)") + } + } + // MARK: - Schema Migration Insertion Point } diff --git a/SignalServiceKit/src/Storage/Database/Records/InteractionFinder.swift b/SignalServiceKit/src/Storage/Database/Records/InteractionFinder.swift index 391cd76e09..0a56c12ad5 100644 --- a/SignalServiceKit/src/Storage/Database/Records/InteractionFinder.swift +++ b/SignalServiceKit/src/Storage/Database/Records/InteractionFinder.swift @@ -5,6 +5,12 @@ import Foundation import GRDB +public enum StoryReplyQueryMode { + case includeAllReplies + case excludeGroupReplies + case onlyGroupReplies(storyTimestamp: UInt64) +} + protocol InteractionFinderAdapter { associatedtype ReadTransaction @@ -31,6 +37,11 @@ protocol InteractionFinderAdapter { static func interactions(withInteractionIds interactionIds: Set, transaction: ReadTransaction) -> Set + static func enumerateGroupReplies(for storyMessage: StoryMessage, transaction: ReadTransaction, block: @escaping (TSMessage, UnsafeMutablePointer) -> Void) + static func countReplies(for storyMessage: StoryMessage, transaction: ReadTransaction) -> UInt + static func hasReplies(for storyContext: StoryContext, transaction: ReadTransaction) -> Bool + static func groupReplyUniqueIds(for storyMessage: StoryMessage, transaction: ReadTransaction) -> [String] + // MARK: - instance methods func latestInteraction(from address: SignalServiceAddress, transaction: ReadTransaction) -> TSInteraction? @@ -39,12 +50,12 @@ protocol InteractionFinderAdapter { func earliestKnownInteractionRowId(transaction: ReadTransaction) -> Int? - func distanceFromLatest(interactionUniqueId: String, excludingPlaceholders excludePlaceholders: Bool, transaction: ReadTransaction) throws -> UInt? - func count(excludingPlaceholders excludePlaceholders: Bool, transaction: ReadTransaction) -> UInt + func distanceFromLatest(interactionUniqueId: String, excludingPlaceholders excludePlaceholders: Bool, storyReplyQueryMode: StoryReplyQueryMode, transaction: ReadTransaction) throws -> UInt? + func count(excludingPlaceholders excludePlaceholders: Bool, storyReplyQueryMode: StoryReplyQueryMode, transaction: ReadTransaction) -> UInt func enumerateInteractionIds(transaction: ReadTransaction, block: @escaping (String, UnsafeMutablePointer) throws -> Void) throws func enumerateRecentInteractions(transaction: ReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer) -> Void) throws - func enumerateInteractions(range: NSRange, excludingPlaceholders excludePlaceholders: Bool, transaction: ReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer) -> Void) throws - func interactionIds(inRange range: NSRange, excludingPlaceholders excludePlaceholders: Bool, transaction: ReadTransaction) throws -> [String] + func enumerateInteractions(range: NSRange, excludingPlaceholders excludePlaceholders: Bool, storyReplyQueryMode: StoryReplyQueryMode, transaction: ReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer) -> Void) throws + func interactionIds(inRange range: NSRange, excludingPlaceholders excludePlaceholders: Bool, storyReplyQueryMode: StoryReplyQueryMode, transaction: ReadTransaction) throws -> [String] func existsOutgoingMessage(transaction: ReadTransaction) -> Bool func outgoingMessageCount(transaction: ReadTransaction) -> UInt @@ -205,6 +216,34 @@ public class InteractionFinder: NSObject, InteractionFinderAdapter { } } + public static func enumerateGroupReplies(for storyMessage: StoryMessage, transaction: SDSAnyReadTransaction, block: @escaping (TSMessage, UnsafeMutablePointer) -> Void) { + switch transaction.readTransaction { + case .grdbRead(let grdbRead): + GRDBInteractionFinder.enumerateGroupReplies(for: storyMessage, transaction: grdbRead, block: block) + } + } + + public static func countReplies(for storyMessage: StoryMessage, transaction: SDSAnyReadTransaction) -> UInt { + switch transaction.readTransaction { + case .grdbRead(let grdbRead): + return GRDBInteractionFinder.countReplies(for: storyMessage, transaction: grdbRead) + } + } + + public static func hasReplies(for storyContext: StoryContext, transaction: SDSAnyReadTransaction) -> Bool { + switch transaction.readTransaction { + case .grdbRead(let grdbRead): + return GRDBInteractionFinder.hasReplies(for: storyContext, transaction: grdbRead) + } + } + + public static func groupReplyUniqueIds(for storyMessage: StoryMessage, transaction: SDSAnyReadTransaction) -> [String] { + switch transaction.readTransaction { + case .grdbRead(let grdbRead): + return GRDBInteractionFinder.groupReplyUniqueIds(for: storyMessage, transaction: grdbRead) + } + } + @objc public class func findMessage( withTimestamp timestamp: UInt64, @@ -287,21 +326,20 @@ public class InteractionFinder: NSObject, InteractionFinderAdapter { } } - public func distanceFromLatest(interactionUniqueId: String, excludingPlaceholders excludePlaceholders: Bool = true, transaction: SDSAnyReadTransaction) throws -> UInt? { - return try Bench(title: "InteractionFinder.distanceFromLatestExcludingPlaceholders_\(excludePlaceholders)") { + public func distanceFromLatest(interactionUniqueId: String, excludingPlaceholders excludePlaceholders: Bool = true, storyReplyQueryMode: StoryReplyQueryMode = .excludeGroupReplies, transaction: SDSAnyReadTransaction) throws -> UInt? { + return try Bench(title: "InteractionFinder.distanceFromLatestExcludingPlaceholders_\(excludePlaceholders)_StoryReplyQueryMode_\(storyReplyQueryMode)") { switch transaction.readTransaction { case .grdbRead(let grdbRead): - return try grdbAdapter.distanceFromLatest(interactionUniqueId: interactionUniqueId, excludingPlaceholders: excludePlaceholders, transaction: grdbRead) + return try grdbAdapter.distanceFromLatest(interactionUniqueId: interactionUniqueId, excludingPlaceholders: excludePlaceholders, storyReplyQueryMode: storyReplyQueryMode, transaction: grdbRead) } } } - @objc(countExcludingPlaceholders:transaction:) - public func count(excludingPlaceholders excludePlaceholders: Bool = true, transaction: SDSAnyReadTransaction) -> UInt { - return Bench(title: "InteractionFinder.countExcludingPlaceholders_\(excludePlaceholders)") { + public func count(excludingPlaceholders excludePlaceholders: Bool = true, storyReplyQueryMode: StoryReplyQueryMode = .excludeGroupReplies, transaction: SDSAnyReadTransaction) -> UInt { + return Bench(title: "InteractionFinder.countExcludingPlaceholders_\(excludePlaceholders)_StoryReplyQueryMode_\(storyReplyQueryMode)") { switch transaction.readTransaction { case .grdbRead(let grdbRead): - return grdbAdapter.count(excludingPlaceholders: excludePlaceholders, transaction: grdbRead) + return grdbAdapter.count(excludingPlaceholders: excludePlaceholders, storyReplyQueryMode: storyReplyQueryMode, transaction: grdbRead) } } } @@ -353,20 +391,20 @@ public class InteractionFinder: NSObject, InteractionFinderAdapter { } } - public func enumerateInteractions(range: NSRange, excludingPlaceholders excludePlaceholders: Bool = true, transaction: SDSAnyReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer) -> Void) throws { - return try Bench(title: "InteractionFinder.enumerateInteractionsInRangeExcludingPlaceholders_\(excludePlaceholders)") { + public func enumerateInteractions(range: NSRange, excludingPlaceholders excludePlaceholders: Bool = true, storyReplyQueryMode: StoryReplyQueryMode = .excludeGroupReplies, transaction: SDSAnyReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer) -> Void) throws { + return try Bench(title: "InteractionFinder.enumerateInteractionsInRangeExcludingPlaceholders_\(excludePlaceholders)_StoryReplyQueryMode_\(storyReplyQueryMode)") { switch transaction.readTransaction { case .grdbRead(let grdbRead): - return try grdbAdapter.enumerateInteractions(range: range, excludingPlaceholders: excludePlaceholders, transaction: grdbRead, block: block) + return try grdbAdapter.enumerateInteractions(range: range, excludingPlaceholders: excludePlaceholders, storyReplyQueryMode: storyReplyQueryMode, transaction: grdbRead, block: block) } } } - public func interactionIds(inRange range: NSRange, excludingPlaceholders excludePlaceholders: Bool = true, transaction: SDSAnyReadTransaction) throws -> [String] { - return try Bench(title: "InteractionFinder.interactionsIdsInRangeExcludingPlaceholders_\(excludePlaceholders)") { + public func interactionIds(inRange range: NSRange, excludingPlaceholders excludePlaceholders: Bool = true, storyReplyQueryMode: StoryReplyQueryMode = .excludeGroupReplies, transaction: SDSAnyReadTransaction) throws -> [String] { + return try Bench(title: "InteractionFinder.interactionsIdsInRangeExcludingPlaceholders_\(excludePlaceholders)_StoryReplyQueryMode_\(storyReplyQueryMode)") { switch transaction.readTransaction { case .grdbRead(let grdbRead): - return try grdbAdapter.interactionIds(inRange: range, excludingPlaceholders: excludePlaceholders, transaction: grdbRead) + return try grdbAdapter.interactionIds(inRange: range, excludingPlaceholders: excludePlaceholders, storyReplyQueryMode: storyReplyQueryMode, transaction: grdbRead) } } } @@ -379,7 +417,7 @@ public class InteractionFinder: NSObject, InteractionFinderAdapter { SELECT * FROM \(InteractionRecord.databaseTableName) WHERE \(interactionColumn: .threadUniqueId) = ? - AND \(sqlClauseForAllUnreadInteractions) + AND \(sqlClauseForAllUnreadInteractions()) ORDER BY \(interactionColumn: .id) """ @@ -405,7 +443,7 @@ public class InteractionFinder: NSObject, InteractionFinderAdapter { FROM \(InteractionRecord.databaseTableName) WHERE \(interactionColumn: .threadUniqueId) = ? AND \(interactionColumn: .id) <= ? - AND \(sqlClauseForAllUnreadInteractions) + AND \(sqlClauseForAllUnreadInteractions()) """ guard let count = try UInt.fetchOne(transaction.database, @@ -432,7 +470,7 @@ public class InteractionFinder: NSObject, InteractionFinderAdapter { FROM \(InteractionRecord.databaseTableName) WHERE \(interactionColumn: .threadUniqueId) = ? AND \(interactionColumn: .id) <= ? - AND \(sqlClauseForAllUnreadInteractions) + AND \(sqlClauseForAllUnreadInteractions()) ORDER BY \(interactionColumn: .id) """ @@ -500,12 +538,12 @@ public class InteractionFinder: NSObject, InteractionFinderAdapter { return cursor.compactMap { $0 as? TSOutgoingMessage } } - public func oldestUnreadInteraction(transaction: GRDBReadTransaction) throws -> TSInteraction? { + public func oldestUnreadInteraction(storyReplyQueryMode: StoryReplyQueryMode, transaction: GRDBReadTransaction) throws -> TSInteraction? { let sql = """ SELECT * FROM \(InteractionRecord.databaseTableName) WHERE \(interactionColumn: .threadUniqueId) = ? - AND \(sqlClauseForAllUnreadInteractions) + AND \(sqlClauseForAllUnreadInteractions(for: storyReplyQueryMode)) ORDER BY \(interactionColumn: .id) """ let cursor = TSInteraction.grdbFetchCursor(sql: sql, arguments: [threadUniqueId], transaction: transaction) @@ -555,7 +593,7 @@ public class InteractionFinder: NSObject, InteractionFinderAdapter { // MARK: - Unread - private let sqlClauseForAllUnreadInteractions: String = { + private func sqlClauseForAllUnreadInteractions(for storyReplyQueryMode: StoryReplyQueryMode = .excludeGroupReplies) -> String { let recordTypes: [SDSRecordType] = [ .disappearingConfigurationUpdateInfoMessage, .unknownProtocolVersionMessage, @@ -575,10 +613,11 @@ public class InteractionFinder: NSObject, InteractionFinderAdapter { return """ ( \(interactionColumn: .read) IS 0 + \(GRDBInteractionFinder.filterStoryRepliesClause(for: storyReplyQueryMode)) AND \(interactionColumn: .recordType) IN (\(recordTypesSql)) ) """ - }() + } private static func sqlClauseForUnreadInteractionCounts(interactionsAlias: String? = nil) -> String { let columnPrefix: String @@ -590,6 +629,7 @@ public class InteractionFinder: NSObject, InteractionFinderAdapter { return """ \(columnPrefix)\(interactionColumn: .read) IS 0 + \(GRDBInteractionFinder.filterStoryRepliesClause(for: .excludeGroupReplies, interactionsAlias: interactionsAlias)) AND ( \(columnPrefix)\(interactionColumn: .recordType) IN (\(SDSRecordType.incomingMessage.rawValue), \(SDSRecordType.call.rawValue)) OR ( @@ -882,6 +922,112 @@ public class GRDBInteractionFinder: NSObject, InteractionFinderAdapter { return interactions } + static func enumerateGroupReplies(for storyMessage: StoryMessage, transaction: ReadTransaction, block: @escaping (TSMessage, UnsafeMutablePointer) -> Void) { + let sql = """ + SELECT * + FROM \(InteractionRecord.databaseTableName) + WHERE \(interactionColumn: .storyTimestamp) = ? + AND \(interactionColumn: .storyAuthorUuidString) = ? + AND \(interactionColumn: .isGroupStoryReply) = 1 + """ + let cursor = TSInteraction.grdbFetchCursor( + sql: sql, + arguments: [storyMessage.timestamp, storyMessage.authorUuid.uuidString], + transaction: transaction + ) + do { + while let interaction = try cursor.next() { + guard let message = interaction as? TSMessage else { + owsFailDebug("Unexpected object: \(type(of: interaction))") + return + } + var stop: ObjCBool = false + block(message, &stop) + if stop.boolValue { + return + } + } + } catch { + owsFail("error: \(error)") + } + } + + static func countReplies(for storyMessage: StoryMessage, transaction: GRDBReadTransaction) -> UInt { + do { + let sql: String = """ + SELECT COUNT(*) + FROM \(InteractionRecord.databaseTableName) + WHERE \(interactionColumn: .storyTimestamp) = ? + AND \(interactionColumn: .storyAuthorUuidString) = ? + """ + guard let count = try UInt.fetchOne( + transaction.database, + sql: sql, + arguments: [storyMessage.timestamp, storyMessage.authorUuid.uuidString] + ) else { + throw OWSAssertionError("count was unexpectedly nil") + } + return count + } catch { + owsFail("error: \(error)") + } + } + + static func hasReplies(for storyContext: StoryContext, transaction: GRDBReadTransaction) -> Bool { + let threadUniqueId: String + switch storyContext { + case .groupId(let data): + threadUniqueId = TSGroupThread.threadId( + forGroupId: data, + transaction: transaction.asAnyRead + ) + case .authorUuid(let uuid): + guard let contactThread = TSContactThread.getWithContactAddress( + SignalServiceAddress(uuid: uuid), + transaction: transaction.asAnyRead + ) else { return false } + threadUniqueId = contactThread.uniqueId + case .none: + return false + } + + let sql = """ + SELECT EXISTS( + SELECT 1 + FROM \(InteractionRecord.databaseTableName) + WHERE \(interactionColumn: .threadUniqueId) = ? + AND \(interactionColumn: .storyTimestamp) IS NOT NULL + AND \(interactionColumn: .storyAuthorUuidString) IS NOT NULL + LIMIT 1 + ) + """ + do { + return try Bool.fetchOne(transaction.database, sql: sql, arguments: [threadUniqueId]) ?? false + } catch { + owsFail("error: \(error)") + } + } + + static func groupReplyUniqueIds(for storyMessage: StoryMessage, transaction: GRDBReadTransaction) -> [String] { + do { + let sql: String = """ + SELECT \(interactionColumn: .uniqueId) + FROM \(InteractionRecord.databaseTableName) + WHERE \(interactionColumn: .storyTimestamp) = ? + AND \(interactionColumn: .storyAuthorUuidString) = ? + AND \(interactionColumn: .isGroupStoryReply) = 1 + ORDER BY \(interactionColumn: .id) ASC + """ + return try String.fetchAll( + transaction.database, + sql: sql, + arguments: [storyMessage.timestamp, storyMessage.authorUuid.uuidString] + ) + } catch { + owsFail("error: \(error)") + } + } + static func oldestPlaceholderInteraction(transaction: GRDBReadTransaction) -> OWSRecoverableDecryptionPlaceholder? { let sql = """ SELECT * @@ -990,6 +1136,7 @@ public class GRDBInteractionFinder: NSObject, InteractionFinderAdapter { SELECT * FROM \(InteractionRecord.databaseTableName) WHERE \(interactionColumn: .threadUniqueId) = ? + \(Self.filterStoryRepliesClause(for: .excludeGroupReplies)) AND \(interactionColumn: .errorType) IS NOT ? AND \(interactionColumn: .messageType) IS NOT ? AND \(interactionColumn: .messageType) IS NOT ? @@ -1047,7 +1194,25 @@ public class GRDBInteractionFinder: NSObject, InteractionFinderAdapter { // If you need to adjust this clause, you should probably update the index as well. This is a perf sensitive code path. private let filterPlaceholdersClause = "AND \(interactionColumn: .recordType) IS NOT \(SDSRecordType.recoverableDecryptionPlaceholder.rawValue)" - func distanceFromLatest(interactionUniqueId: String, excludingPlaceholders excludePlaceholders: Bool = true, transaction: GRDBReadTransaction) throws -> UInt? { + fileprivate static func filterStoryRepliesClause(for queryMode: StoryReplyQueryMode, interactionsAlias: String? = nil) -> String { + let columnPrefix: String + if let interactionsAlias = interactionsAlias { + columnPrefix = interactionsAlias + "." + } else { + columnPrefix = "" + } + + switch queryMode { + case .excludeGroupReplies: + return "AND \(columnPrefix)\(interactionColumn: .isGroupStoryReply) = 0" + case .onlyGroupReplies(let storyTimestamp): + return "AND \(columnPrefix)\(interactionColumn: .isGroupStoryReply) = 1 AND \(columnPrefix)\(interactionColumn: .storyTimestamp) = \(storyTimestamp)" + case .includeAllReplies: + return "" + } + } + + func distanceFromLatest(interactionUniqueId: String, excludingPlaceholders excludePlaceholders: Bool = true, storyReplyQueryMode: StoryReplyQueryMode = .excludeGroupReplies, transaction: GRDBReadTransaction) throws -> UInt? { let fetchInteractionIdSQL = """ SELECT id @@ -1069,6 +1234,7 @@ public class GRDBInteractionFinder: NSObject, InteractionFinderAdapter { FROM \(InteractionRecord.databaseTableName) WHERE \(interactionColumn: .threadUniqueId) = ? AND \(interactionColumn: .id) >= ? + \(Self.filterStoryRepliesClause(for: storyReplyQueryMode)) \(excludePlaceholders ? filterPlaceholdersClause : "") """ let distanceArguments: StatementArguments = [threadUniqueId, interactionId] @@ -1084,12 +1250,13 @@ public class GRDBInteractionFinder: NSObject, InteractionFinderAdapter { return distanceFromLatest } - func count(excludingPlaceholders excludePlaceholders: Bool = true, transaction: GRDBReadTransaction) -> UInt { + func count(excludingPlaceholders excludePlaceholders: Bool = true, storyReplyQueryMode: StoryReplyQueryMode = .excludeGroupReplies, transaction: GRDBReadTransaction) -> UInt { do { let sql: String = """ SELECT COUNT(*) FROM \(InteractionRecord.databaseTableName) WHERE \(interactionColumn: .threadUniqueId) = ? + \(Self.filterStoryRepliesClause(for: storyReplyQueryMode)) \(excludePlaceholders ? filterPlaceholdersClause : "") """ let arguments: StatementArguments = [threadUniqueId] @@ -1142,11 +1309,12 @@ public class GRDBInteractionFinder: NSObject, InteractionFinderAdapter { } } - func enumerateInteractions(range: NSRange, excludingPlaceholders excludePlaceholders: Bool = true, transaction: GRDBReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer) -> Void) throws { + func enumerateInteractions(range: NSRange, excludingPlaceholders excludePlaceholders: Bool = true, storyReplyQueryMode: StoryReplyQueryMode = .excludeGroupReplies, transaction: GRDBReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer) -> Void) throws { let sql = """ SELECT * FROM \(InteractionRecord.databaseTableName) WHERE \(interactionColumn: .threadUniqueId) = ? + \(Self.filterStoryRepliesClause(for: storyReplyQueryMode)) \(excludePlaceholders ? filterPlaceholdersClause : "") ORDER BY \(interactionColumn: .id) LIMIT \(range.length) @@ -1166,11 +1334,12 @@ public class GRDBInteractionFinder: NSObject, InteractionFinderAdapter { } } - func interactionIds(inRange range: NSRange, excludingPlaceholders excludePlaceholders: Bool = true, transaction: GRDBReadTransaction) throws -> [String] { + func interactionIds(inRange range: NSRange, excludingPlaceholders excludePlaceholders: Bool = true, storyReplyQueryMode: StoryReplyQueryMode = .excludeGroupReplies, transaction: GRDBReadTransaction) throws -> [String] { let sql = """ SELECT \(interactionColumn: .uniqueId) FROM \(InteractionRecord.databaseTableName) WHERE \(interactionColumn: .threadUniqueId) = ? + \(Self.filterStoryRepliesClause(for: storyReplyQueryMode)) \(excludePlaceholders ? filterPlaceholdersClause : "") ORDER BY \(interactionColumn: .id) LIMIT \(range.length) @@ -1350,6 +1519,7 @@ public class GRDBInteractionFinder: NSObject, InteractionFinderAdapter { AND \(interactionColumn: .errorType) IN (\(errorMessageTypes.map { "\($0.rawValue)" }.joined(separator: ","))) ) OR \(interactionColumn: .recordType) IN (\(interactionTypes.map { "\($0.rawValue)" }.joined(separator: ","))) ) + \(Self.filterStoryRepliesClause(for: .excludeGroupReplies)) LIMIT 1 ) """ diff --git a/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift b/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift index e7f5d7c4a4..7c7892b386 100644 --- a/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift +++ b/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift @@ -273,6 +273,7 @@ public class SDSDatabaseStorage: SDSTransactable { } } + @objc(touchStoryMessage:transaction:) public func touch(storyMessage: StoryMessage, transaction: SDSAnyWriteTransaction) { switch transaction.writeTransaction { case .grdbWrite(let grdb): diff --git a/SignalServiceKit/src/TestUtils/Factories.swift b/SignalServiceKit/src/TestUtils/Factories.swift index 02d53c648e..6e541da6dd 100644 --- a/SignalServiceKit/src/TestUtils/Factories.swift +++ b/SignalServiceKit/src/TestUtils/Factories.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation @@ -139,7 +139,10 @@ public class OutgoingMessageFactory: NSObject, Factory { messageSticker: messageStickerBuilder(), isViewOnceMessage: isViewOnceMessageBuilder(), changeActionsProtoData: changeActionsProtoDataBuilder(), - additionalRecipients: additionalRecipientsBuilder()).build() + additionalRecipients: additionalRecipientsBuilder(), + storyAuthorAddress: storyAuthorAddressBuilder(), + storyTimestamp: storyTimestampBuilder(), + storyReactionEmoji: storyReactionEmojiBuilder()).build() } @objc @@ -234,6 +237,21 @@ public class OutgoingMessageFactory: NSObject, Factory { return nil } + @objc + public var storyAuthorAddressBuilder: () -> SignalServiceAddress? = { + return nil + } + + @objc + public var storyTimestampBuilder: () -> NSNumber? = { + return nil + } + + @objc + public var storyReactionEmojiBuilder: () -> String? = { + return nil + } + // MARK: Delivery Receipts @objc @@ -288,7 +306,10 @@ public class IncomingMessageFactory: NSObject, Factory { serverDeliveryTimestamp: serverDeliveryTimestampBuilder(), serverGuid: serverGuidBuilder(), wasReceivedByUD: wasReceivedByUDBuilder(), - isViewOnceMessage: isViewOnceMessageBuilder()) + isViewOnceMessage: isViewOnceMessageBuilder(), + storyAuthorAddress: storyAuthorAddressBuilder(), + storyTimestamp: storyTimestampBuilder(), + storyReactionEmoji: storyReactionEmojiBuilder()) let item = builder.build() item.anyInsert(transaction: transaction) return item @@ -391,6 +412,21 @@ public class IncomingMessageFactory: NSObject, Factory { public var isViewOnceMessageBuilder: () -> Bool = { return false } + + @objc + public var storyAuthorAddressBuilder: () -> SignalServiceAddress? = { + nil + } + + @objc + public var storyTimestampBuilder: () -> NSNumber? = { + nil + } + + @objc + public var storyReactionEmojiBuilder: () -> String? = { + return nil + } } @objc diff --git a/SignalServiceKit/src/Util/Refinery.swift b/SignalServiceKit/src/Util/Refinery.swift index 4151b48a65..d9dd1b8e56 100644 --- a/SignalServiceKit/src/Util/Refinery.swift +++ b/SignalServiceKit/src/Util/Refinery.swift @@ -115,3 +115,13 @@ public class Refinery { return AnySequence(indexes.lazy.map { keys[$0] }) } } + +public extension Dictionary { + init>(_ refinery: T) { + let keysAndValues: [(Key, Value)] = zip(refinery.keys, refinery.values).compactMap { key, value in + guard let value = value else { return nil } + return (key, value) + } + self.init(uniqueKeysWithValues: keysAndValues) + } +} diff --git a/SignalServiceKit/src/groups/GroupManager.swift b/SignalServiceKit/src/groups/GroupManager.swift index 251fee45f3..ca7477880f 100644 --- a/SignalServiceKit/src/groups/GroupManager.swift +++ b/SignalServiceKit/src/groups/GroupManager.swift @@ -1157,7 +1157,7 @@ public class GroupManager: NSObject { sendGroupQuitMessage(inThread: groupThread, transaction: transaction) - let hasMessages = groupThread.numberOfInteractions(with: transaction) > 0 + let hasMessages = groupThread.numberOfInteractions(transaction: transaction) > 0 let infoMessagePolicy: InfoMessagePolicy = hasMessages ? .always : .never var groupMembershipBuilder = oldGroupModel.groupMembership.asBuilder diff --git a/SignalUI/Views/InputAccessoryViewPlaceholder.swift b/SignalUI/Views/InputAccessoryViewPlaceholder.swift index 7cc0981db6..fbafa3e81b 100644 --- a/SignalUI/Views/InputAccessoryViewPlaceholder.swift +++ b/SignalUI/Views/InputAccessoryViewPlaceholder.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation @@ -36,7 +36,7 @@ public class InputAccessoryViewPlaceholder: UIView { } @objc - weak var referenceView: UIView? + public weak var referenceView: UIView? private var visibleKeyboardHeight: CGFloat { guard var keyboardFrame = transitioningKeyboardFrame ?? superview?.frame else { return 0 } diff --git a/SignalUI/Views/Mentions/Mention.swift b/SignalUI/Views/Mentions/Mention.swift index 4c55d59c02..db0d2bf7c0 100644 --- a/SignalUI/Views/Mentions/Mention.swift +++ b/SignalUI/Views/Mentions/Mention.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation @@ -24,6 +24,7 @@ public class Mention: NSObject { case composingAttachment case quotedReply case longMessageView + case groupReply public static var composing: Self = .incoming } @@ -79,6 +80,9 @@ public class Mention: NSObject { case .longMessageView: attributes[.backgroundColor] = Theme.isDarkThemeEnabled ? UIColor.ows_signalBlueDark : UIColor.ows_blackAlpha20 attributes[.foregroundColor] = Theme.primaryTextColor + case .groupReply: + attributes[.backgroundColor] = UIColor.ows_gray60 + attributes[.foregroundColor] = UIColor.ows_gray05 } return attributes diff --git a/SignalUI/Views/Mentions/MentionPicker.swift b/SignalUI/Views/Mentions/MentionPicker.swift index 6db1b80105..b90ae57255 100644 --- a/SignalUI/Views/Mentions/MentionPicker.swift +++ b/SignalUI/Views/Mentions/MentionPicker.swift @@ -159,6 +159,22 @@ class MentionPicker: UIView { if style == .composingAttachment { tableView.backgroundColor = UIColor.ows_gray95 hairlineView.backgroundColor = .ows_gray65 + } else if style == .groupReply { + blurView?.removeFromSuperview() + blurView = nil + + if UIAccessibility.isReduceTransparencyEnabled { + tableView.backgroundColor = Theme.darkThemeBackgroundColor + } else { + tableView.backgroundColor = .clear + + let blurView = UIVisualEffectView(effect: Theme.darkThemeBarBlurEffect) + self.blurView = blurView + insertSubview(blurView, belowSubview: tableView) + blurView.autoPinEdgesToSuperviewEdges() + } + + hairlineView.backgroundColor = .ows_gray75 } else { blurView?.removeFromSuperview() blurView = nil @@ -330,7 +346,7 @@ private class MentionableUserCell: UITableViewCell { } func configure(with mentionableUser: MentionPicker.MentionableUser, style: Mention.Style) { - if style == .composingAttachment { + if [.composingAttachment, .groupReply].contains(style) { displayNameLabel.textColor = Theme.darkThemePrimaryColor usernameLabel.textColor = Theme.darkThemeSecondaryTextAndIconColor selectedBackgroundView?.backgroundColor = UIColor.white.withAlphaComponent(0.2)