Merge branch 'nt/stories-replies'

This commit is contained in:
Nora Trapp 2022-03-31 00:34:56 -07:00
commit b3b1fea62e
95 changed files with 2898 additions and 286 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "add-reaction-outline-24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "messages-solid-20.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "reply-solid-20.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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