Merge branch 'nt/stories-replies'
This commit is contained in:
commit
b3b1fea62e
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 = "<group>"; };
|
||||
885C354F2370DFD50004BA35 /* OWSSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSSyncManager.swift; sourceTree = "<group>"; };
|
||||
8862A55825F090C5005D65DB /* InternalSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalSettingsViewController.swift; sourceTree = "<group>"; };
|
||||
8864072727EEA658009916B6 /* StoryGroupReplySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplySheet.swift; sourceTree = "<group>"; };
|
||||
8864072927F0D426009916B6 /* StoryGroupReplyLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyLoader.swift; sourceTree = "<group>"; };
|
||||
8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyViewItem.swift; sourceTree = "<group>"; };
|
||||
8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyCell.swift; sourceTree = "<group>"; };
|
||||
8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryReplyInputToolbar.swift; sourceTree = "<group>"; };
|
||||
886A58C8276A760600A1099B /* SubscriptionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = "<group>"; };
|
||||
886A58C9276A760600A1099B /* SubscriptionReceiptCredentialRedemptionJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionReceiptCredentialRedemptionJob.swift; sourceTree = "<group>"; };
|
||||
886CB07724E77E5B00753909 /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = "<group>"; };
|
||||
@ -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 = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
||||
12
Signal/Images.xcassets/add-reaction-outline-24.imageset/Contents.json
vendored
Normal file
12
Signal/Images.xcassets/add-reaction-outline-24.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "add-reaction-outline-24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/add-reaction-outline-24.imageset/add-reaction-outline-24.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/add-reaction-outline-24.imageset/add-reaction-outline-24.pdf
vendored
Normal file
Binary file not shown.
12
Signal/Images.xcassets/messages-solid-20.imageset/Contents.json
vendored
Normal file
12
Signal/Images.xcassets/messages-solid-20.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "messages-solid-20.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/messages-solid-20.imageset/messages-solid-20.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/messages-solid-20.imageset/messages-solid-20.pdf
vendored
Normal file
Binary file not shown.
12
Signal/Images.xcassets/reply-solid-20.imageset/Contents.json
vendored
Normal file
12
Signal/Images.xcassets/reply-solid-20.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "reply-solid-20.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/reply-solid-20.imageset/reply-solid-20.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/reply-solid-20.imageset/reply-solid-20.pdf
vendored
Normal file
Binary file not shown.
@ -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")
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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..<numberOfItems {
|
||||
if itemState.index == x, itemState.value < 1, itemState.value > 0 {
|
||||
for idx in 0..<numberOfItems {
|
||||
if itemState.index == idx, itemState.value < 1, itemState.value > 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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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<String>? = nil,
|
||||
deletedInteractionIds: Set<String>? = 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<String>? = nil,
|
||||
deletedInteractionIds: Set<String>? = 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<SignalServiceAddress>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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" = "<bold>1</bold> reply";
|
||||
|
||||
/* Button for replying to a story with N existing replies. {embeds the number of replies} */
|
||||
"STORY_REPLY_BUTTON_WITH_N_REPLIES_FORMAT" = "<bold>%@</bold> 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";
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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<SignalServiceAddress, String> {
|
||||
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<SignalServiceAddress, String>(addresses).refine {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalServiceKit/BaseModel.h>
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalServiceKit/TSInfoMessage.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<InfoMessageUserInfoKey, id> *)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
|
||||
|
||||
|
||||
@ -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<InfoMessageUserInfoKey, id> *)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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalServiceKit/OWSRecipientIdentity.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<InfoMessageUserInfoKey, id> *)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
|
||||
|
||||
|
||||
@ -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<InfoMessageUserInfoKey, id> *)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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalServiceKit/OWSReadTracking.h>
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalServiceKit/OWSReadTracking.h>
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalServiceKit/OWSReadTracking.h>
|
||||
@ -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<InfoMessageUserInfoKey, id> *)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
|
||||
|
||||
|
||||
@ -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<InfoMessageUserInfoKey, id> *)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) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalServiceKit/OWSContact.h>
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<SignalServiceAddress *,TSOutgoingMessageRecipientState *> *)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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalServiceKit/TSInvalidIdentityKeyErrorMessage.h>
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalServiceKit/TSInvalidIdentityKeyErrorMessage.h>
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalServiceKit/TSInfoMessage.h>
|
||||
@ -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<InfoMessageUserInfoKey, id> *)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
|
||||
|
||||
|
||||
@ -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<InfoMessageUserInfoKey, id> *)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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalServiceKit/TSOutgoingMessage.h>
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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<String>, transaction: ReadTransaction) -> Set<TSInteraction>
|
||||
|
||||
static func enumerateGroupReplies(for storyMessage: StoryMessage, transaction: ReadTransaction, block: @escaping (TSMessage, UnsafeMutablePointer<ObjCBool>) -> 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<ObjCBool>) throws -> Void) throws
|
||||
func enumerateRecentInteractions(transaction: ReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer<ObjCBool>) -> Void) throws
|
||||
func enumerateInteractions(range: NSRange, excludingPlaceholders excludePlaceholders: Bool, transaction: ReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer<ObjCBool>) -> 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<ObjCBool>) -> 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<ObjCBool>) -> 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<ObjCBool>) -> 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<ObjCBool>) -> 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<ObjCBool>) -> 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<ObjCBool>) -> Void) throws {
|
||||
func enumerateInteractions(range: NSRange, excludingPlaceholders excludePlaceholders: Bool = true, storyReplyQueryMode: StoryReplyQueryMode = .excludeGroupReplies, transaction: GRDBReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer<ObjCBool>) -> 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
|
||||
)
|
||||
"""
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -115,3 +115,13 @@ public class Refinery<Key, Value> {
|
||||
return AnySequence(indexes.lazy.map { keys[$0] })
|
||||
}
|
||||
}
|
||||
|
||||
public extension Dictionary {
|
||||
init<T: Refinery<Key, Value>>(_ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user