diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponentDelegate.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponentDelegate.swift index 075586563d..ead087b090 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponentDelegate.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponentDelegate.swift @@ -16,6 +16,8 @@ public protocol CVComponentDelegate { func cvc_enqueueReload() + func cvc_enqueueReloadWithoutCaches() + // MARK: - Body Text Items func cvc_didTapBodyTextItem(_ item: CVBodyTextLabel.ItemObject) diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentAudioAttachment.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentAudioAttachment.swift index 8ef6e7c511..756194bee0 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentAudioAttachment.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentAudioAttachment.swift @@ -73,8 +73,11 @@ public class CVComponentAudioAttachment: CVComponentBase, CVComponent { } else if let outgoingMessage = interaction as? TSOutgoingMessage { audioMessageView.setViewed(!outgoingMessage.viewedRecipientAddresses().isEmpty, animated: false) } - audioMessageView.configureForRendering(cellMeasurement: cellMeasurement, - conversationStyle: conversationStyle) + audioMessageView.configureForRendering( + cellMeasurement: cellMeasurement, + conversationStyle: conversationStyle, + audioPlaybackRate: itemViewState.audioPlaybackRate + ) componentView.audioMessageView = audioMessageView stackView.configure(config: stackViewConfig, cellMeasurement: cellMeasurement, @@ -108,11 +111,14 @@ public class CVComponentAudioAttachment: CVComponentBase, CVComponent { measurementBuilder.setSize(key: Self.measurementKey_footerSize, size: footerSize) } - let audioSize = AudioMessageView.measure(maxWidth: maxWidth, - audioAttachment: audioAttachment, - isIncoming: isIncoming, - conversationStyle: conversationStyle, - measurementBuilder: measurementBuilder).ceil + let audioSize = AudioMessageView.measure( + maxWidth: maxWidth, + audioAttachment: audioAttachment, + isIncoming: isIncoming, + conversationStyle: conversationStyle, + audioPlaybackRate: itemViewState.audioPlaybackRate, + measurementBuilder: measurementBuilder + ).ceil let audioInfo = audioSize.asManualSubviewInfo let stackMeasurement = ManualStackView.measure(config: stackViewConfig, measurementBuilder: measurementBuilder, diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVItemModel.swift b/Signal/src/ViewControllers/ConversationView/CV/CVItemModel.swift index 33d5e68b61..7784cdc56f 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVItemModel.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVItemModel.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation @@ -11,6 +11,7 @@ import Foundation public class CVItemModel: NSObject { public let interaction: TSInteraction public let thread: TSThread + public let threadAssociatedData: ThreadAssociatedData // The item state loaded from the database. public let componentState: CVComponentState @@ -35,12 +36,14 @@ public class CVItemModel: NSObject { init(interaction: TSInteraction, thread: TSThread, + threadAssociatedData: ThreadAssociatedData, componentState: CVComponentState, itemViewState: CVItemViewState, coreState: CVCoreState) { self.interaction = interaction self.thread = thread + self.threadAssociatedData = threadAssociatedData self.componentState = componentState self.itemViewState = itemViewState self.coreState = coreState diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVItemViewState.swift b/Signal/src/ViewControllers/ConversationView/CV/CVItemViewState.swift index 3d9c424ed6..2d08b60ad3 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVItemViewState.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVItemViewState.swift @@ -3,6 +3,7 @@ // import Foundation +import SignalServiceKit // CVItemViewState represents the transient, un-persisted values // that may affect item appearance. @@ -27,6 +28,7 @@ public struct CVItemViewState: Equatable { let bodyTextState: CVComponentBodyText.State? let giftBadgeState: CVComponentGiftBadge.ViewState? let nextAudioAttachment: AudioAttachment? + let audioPlaybackRate: Float let uiMode: ConversationUIMode let previousUIMode: ConversationUIMode @@ -47,6 +49,7 @@ public struct CVItemViewState: Equatable { var bodyTextState: CVComponentBodyText.State? var giftBadgeState: CVComponentGiftBadge.ViewState? var nextAudioAttachment: AudioAttachment? + var audioPlaybackRate: Float = 1 var uiMode: ConversationUIMode = .normal var previousUIMode: ConversationUIMode = .normal @@ -63,6 +66,7 @@ public struct CVItemViewState: Equatable { bodyTextState: bodyTextState, giftBadgeState: giftBadgeState, nextAudioAttachment: nextAudioAttachment, + audioPlaybackRate: audioPlaybackRate, uiMode: uiMode, previousUIMode: previousUIMode) } @@ -191,6 +195,7 @@ struct CVItemModelBuilder: CVItemBuilding, Dependencies { public static func buildStandaloneItem(interaction: TSInteraction, thread: TSThread, + threadAssociatedData: ThreadAssociatedData, threadViewModel: ThreadViewModel, itemBuildingContext: CVItemBuildingContext, transaction: SDSAnyReadTransaction) -> CVItemModel? { @@ -200,6 +205,7 @@ struct CVItemModelBuilder: CVItemBuilding, Dependencies { guard let itemBuilder = Self.itemBuilder(forInteraction: interaction, thread: thread, + threadAssociatedData: threadAssociatedData, itemBuildingContext: itemBuildingContext, componentStateCache: ComponentStateCache()) else { owsFailDebug("Could not create itemBuilder.") @@ -259,6 +265,8 @@ struct CVItemModelBuilder: CVItemBuilding, Dependencies { itemViewState.giftBadgeState = CVComponentGiftBadge.buildViewState(giftBadge) } + itemViewState.audioPlaybackRate = threadViewModel.associatedData.audioPlaybackRate + if interaction.interactionType == .dateHeader { itemViewState.dateHeaderState = CVComponentDateHeader.buildState(interaction: interaction) } @@ -501,6 +509,7 @@ struct CVItemModelBuilder: CVItemBuilding, Dependencies { itemBuildingContext: itemBuildingContext) let item = ItemBuilder(interaction: interaction, thread: thread, + threadAssociatedData: threadAssociatedData, componentState: componentState) items.append(item) } @@ -527,6 +536,7 @@ struct CVItemModelBuilder: CVItemBuilding, Dependencies { itemBuildingContext: itemBuildingContext) let item = ItemBuilder(interaction: interaction, thread: thread, + threadAssociatedData: threadAssociatedData, componentState: componentState) items.append(item) } @@ -571,6 +581,7 @@ struct CVItemModelBuilder: CVItemBuilding, Dependencies { private mutating func addItem(interaction: TSInteraction) -> ItemBuilder? { guard let item = Self.itemBuilder(forInteraction: interaction, thread: thread, + threadAssociatedData: threadAssociatedData, itemBuildingContext: itemBuildingContext, componentStateCache: componentStateCache) else { return nil @@ -603,6 +614,7 @@ struct CVItemModelBuilder: CVItemBuilding, Dependencies { private static func itemBuilder(forInteraction interaction: TSInteraction, thread: TSThread, + threadAssociatedData: ThreadAssociatedData, itemBuildingContext: CVItemBuildingContext, componentStateCache: ComponentStateCache) -> ItemBuilder? { let componentState: CVComponentState @@ -617,6 +629,7 @@ struct CVItemModelBuilder: CVItemBuilding, Dependencies { return ItemBuilder(interaction: interaction, thread: thread, + threadAssociatedData: threadAssociatedData, componentState: componentState) } @@ -689,20 +702,24 @@ fileprivate extension CVMessageMapping { private class ItemBuilder { let interaction: TSInteraction let thread: TSThread + let threadAssociatedData: ThreadAssociatedData let componentState: CVComponentState var itemViewState = CVItemViewState.Builder() required init(interaction: TSInteraction, thread: TSThread, + threadAssociatedData: ThreadAssociatedData, componentState: CVComponentState) { self.interaction = interaction self.thread = thread + self.threadAssociatedData = threadAssociatedData self.componentState = componentState } func build(coreState: CVCoreState) -> CVItemModel { CVItemModel(interaction: interaction, thread: thread, + threadAssociatedData: threadAssociatedData, componentState: componentState, itemViewState: itemViewState.build(), coreState: coreState) diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVLoadContext.swift b/Signal/src/ViewControllers/ConversationView/CV/CVLoadContext.swift index b7ab276c85..7aec25164a 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVLoadContext.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVLoadContext.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation @@ -85,6 +85,7 @@ extension CVItemBuilding { // Convenience Accessors var threadViewModel: ThreadViewModel { itemBuildingContext.threadViewModel } var thread: TSThread { itemBuildingContext.thread } + var threadAssociatedData: ThreadAssociatedData { threadViewModel.associatedData } var viewStateSnapshot: CVViewStateSnapshot { itemBuildingContext.viewStateSnapshot } var conversationStyle: ConversationStyle { itemBuildingContext.conversationStyle } var mediaCache: CVMediaCache { itemBuildingContext.mediaCache } diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVLoader.swift b/Signal/src/ViewControllers/ConversationView/CV/CVLoader.swift index 7c4a4d004c..65a0e78ebe 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVLoader.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVLoader.swift @@ -261,6 +261,7 @@ public class CVLoader: NSObject { @objc public static func buildStandaloneRenderItem(interaction: TSInteraction, thread: TSThread, + threadAssociatedData: ThreadAssociatedData, containerView: UIView, transaction: SDSAnyReadTransaction) -> CVRenderItem? { let chatColor = ChatColors.chatColorForRendering(thread: thread, transaction: transaction) @@ -274,6 +275,7 @@ public class CVLoader: NSObject { mediaCache: CVMediaCache()) return CVLoader.buildStandaloneRenderItem(interaction: interaction, thread: thread, + threadAssociatedData: threadAssociatedData, coreState: coreState, transaction: transaction) } @@ -281,18 +283,21 @@ public class CVLoader: NSObject { @objc public static func buildStandaloneRenderItem(interaction: TSInteraction, thread: TSThread, + threadAssociatedData: ThreadAssociatedData, conversationStyle: ConversationStyle, transaction: SDSAnyReadTransaction) -> CVRenderItem? { let coreState = CVCoreState(conversationStyle: conversationStyle, mediaCache: CVMediaCache()) return CVLoader.buildStandaloneRenderItem(interaction: interaction, thread: thread, + threadAssociatedData: threadAssociatedData, coreState: coreState, transaction: transaction) } private static func buildStandaloneRenderItem(interaction: TSInteraction, thread: TSThread, + threadAssociatedData: ThreadAssociatedData, coreState: CVCoreState, transaction: SDSAnyReadTransaction) -> CVRenderItem? { AssertIsOnMainThread() @@ -308,6 +313,7 @@ public class CVLoader: NSObject { avatarBuilder: avatarBuilder) guard let itemModel = CVItemModelBuilder.buildStandaloneItem(interaction: interaction, thread: thread, + threadAssociatedData: threadAssociatedData, threadViewModel: threadViewModel, itemBuildingContext: itemBuildingContext, transaction: transaction) else { diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVRenderItem.swift b/Signal/src/ViewControllers/ConversationView/CV/CVRenderItem.swift index b5a544a278..74a046407f 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVRenderItem.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVRenderItem.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation diff --git a/Signal/src/ViewControllers/ConversationView/Cells/AudioMessageView.swift b/Signal/src/ViewControllers/ConversationView/Cells/AudioMessageView.swift index 219d52546c..357cf7d08d 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/AudioMessageView.swift +++ b/Signal/src/ViewControllers/ConversationView/Cells/AudioMessageView.swift @@ -60,8 +60,11 @@ class AudioMessageView: ManualStackView { super.init(name: "AudioMessageView") } - public func configureForRendering(cellMeasurement: CVCellMeasurement, - conversationStyle: ConversationStyle) { + public func configureForRendering( + cellMeasurement: CVCellMeasurement, + conversationStyle: ConversationStyle, + audioPlaybackRate: Float + ) { var outerSubviews = [UIView]() @@ -185,11 +188,14 @@ class AudioMessageView: ManualStackView { private static let measurementKey_bottomInnerStack = "CVComponentAudioAttachment.measurementKey_bottomInnerStack" private static let measurementKey_outerStack = "CVComponentAudioAttachment.measurementKey_outerStack" - public static func measure(maxWidth: CGFloat, - audioAttachment: AudioAttachment, - isIncoming: Bool, - conversationStyle: ConversationStyle, - measurementBuilder: CVCellMeasurement.Builder) -> CGSize { + public static func measure( + maxWidth: CGFloat, + audioAttachment: AudioAttachment, + isIncoming: Bool, + conversationStyle: ConversationStyle, + audioPlaybackRate: Float, + measurementBuilder: CVCellMeasurement.Builder + ) -> CGSize { owsAssertDebug(maxWidth > 0) var outerSubviewInfos = [ManualStackSubviewInfo]() diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController+CVComponentDelegate.swift b/Signal/src/ViewControllers/ConversationView/ConversationViewController+CVComponentDelegate.swift index ed50e3861d..ffe929aa69 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController+CVComponentDelegate.swift +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController+CVComponentDelegate.swift @@ -19,6 +19,10 @@ extension ConversationViewController: CVComponentDelegate { self.loadCoordinator.enqueueReload() } + public func cvc_enqueueReloadWithoutCaches() { + self.loadCoordinator.enqueueReloadWithoutCaches() + } + // MARK: - Body Text Items public func cvc_didTapBodyTextItem(_ item: CVBodyTextLabel.ItemObject) { diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index e22c40539e..878939eb55 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -1950,11 +1950,13 @@ typedef NS_CLOSED_ENUM(NSUInteger, MessageContentType) { isAttachmentDownloaded:YES quotedMessage:nil transaction:transaction]; + ThreadAssociatedData *threadAssociatedData = [self createFakeThreadAssociatedData:thread]; OWSAssertDebug(messageToQuote); UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; CVRenderItem *renderItem = [CVLoader buildStandaloneRenderItemWithInteraction:messageToQuote thread:thread + threadAssociatedData:threadAssociatedData containerView:containerView transaction:transaction]; CVItemViewModelImpl *itemViewModel = [[CVItemViewModelImpl alloc] initWithRenderItem:renderItem]; @@ -1975,10 +1977,12 @@ typedef NS_CLOSED_ENUM(NSUInteger, MessageContentType) { messageSticker:nil transaction:transaction]; OWSAssertDebug(messageToQuote); + ThreadAssociatedData *threadAssociatedData = [self createFakeThreadAssociatedData:thread]; UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; CVRenderItem *renderItem = [CVLoader buildStandaloneRenderItemWithInteraction:messageToQuote thread:thread + threadAssociatedData:threadAssociatedData containerView:containerView transaction:transaction]; CVItemViewModelImpl *itemViewModel = [[CVItemViewModelImpl alloc] initWithRenderItem:renderItem]; @@ -4467,6 +4471,15 @@ typedef OWSContact * (^OWSContactBlock)(SDSAnyWriteTransaction *transaction); return label; } ++ (ThreadAssociatedData *)createFakeThreadAssociatedData:(TSThread *)thread +{ + return [[ThreadAssociatedData alloc] initWithThreadUniqueId:thread.uniqueId + isArchived:NO + isMarkedUnread:NO + mutedUntilTimestamp:0 + audioPlaybackRate:1]; +} + + (TSOutgoingMessage *)createFakeOutgoingMessage:(TSThread *)thread messageBody:(nullable NSString *)messageBody fakeAssetLoader:(nullable DebugUIMessagesAssetLoader *)fakeAssetLoader diff --git a/Signal/src/ViewControllers/MediaGallery/MediaPageViewController.swift b/Signal/src/ViewControllers/MediaGallery/MediaPageViewController.swift index 972880a4aa..5cafe6fbe2 100644 --- a/Signal/src/ViewControllers/MediaGallery/MediaPageViewController.swift +++ b/Signal/src/ViewControllers/MediaGallery/MediaPageViewController.swift @@ -646,8 +646,11 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou owsFailDebug("Missing thread.") return nil } + let threadAssociatedData = ThreadAssociatedData.fetchOrDefault(for: thread, + transaction: transaction) return CVLoader.buildStandaloneRenderItem(interaction: interaction, thread: thread, + threadAssociatedData: threadAssociatedData, containerView: self.view, transaction: transaction) } diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index 8ac0ffc6ab..ee287c5a29 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -205,6 +205,7 @@ class MessageDetailViewController: OWSTableViewController2 { owsFailDebug("Missing thread.") return nil } + let threadAssociatedData = ThreadAssociatedData.fetchOrDefault(for: thread, transaction: transaction) let chatColor = ChatColors.chatColorForRendering(thread: thread, transaction: transaction) @@ -220,6 +221,7 @@ class MessageDetailViewController: OWSTableViewController2 { return CVLoader.buildStandaloneRenderItem( interaction: interaction, thread: thread, + threadAssociatedData: threadAssociatedData, conversationStyle: conversationStyle, transaction: transaction ) @@ -883,6 +885,10 @@ extension MessageDetailViewController: CVComponentDelegate { self.refreshContent() } + func cvc_enqueueReloadWithoutCaches() { + self.refreshContentForDatabaseUpdate() + } + // MARK: - Body Text Items func cvc_didTapBodyTextItem(_ item: CVBodyTextLabel.ItemObject) {} diff --git a/Signal/src/views/MockConversationView.swift b/Signal/src/views/MockConversationView.swift index 0ed6d121a9..bbd520d378 100644 --- a/Signal/src/views/MockConversationView.swift +++ b/Signal/src/views/MockConversationView.swift @@ -128,6 +128,7 @@ class MockConversationView: UIView { isWallpaperPhoto: false, chatColor: chatColor ) + let threadAssociatedData = ThreadAssociatedData.fetchOrDefault(for: thread, transaction: transaction) for item in model.items { let interaction: TSInteraction switch item { @@ -142,6 +143,7 @@ class MockConversationView: UIView { guard let renderItem = CVLoader.buildStandaloneRenderItem( interaction: interaction, thread: self.thread, + threadAssociatedData: threadAssociatedData, conversationStyle: conversationStyle, transaction: transaction ) else { @@ -273,6 +275,8 @@ extension MockConversationView: CVComponentDelegate { func cvc_enqueueReload() {} + func cvc_enqueueReloadWithoutCaches() {} + func cvc_didTapBodyTextItem(_ item: CVBodyTextLabel.ItemObject) {} func cvc_didLongPressBodyTextItem(_ item: CVBodyTextLabel.ItemObject) {} diff --git a/SignalServiceKit/src/Contacts/ThreadAssociatedData.swift b/SignalServiceKit/src/Contacts/ThreadAssociatedData.swift index f45d2fa07e..e2cf883bbc 100644 --- a/SignalServiceKit/src/Contacts/ThreadAssociatedData.swift +++ b/SignalServiceKit/src/Contacts/ThreadAssociatedData.swift @@ -126,6 +126,7 @@ public class ThreadAssociatedData: NSObject, Codable, FetchableRecord, Persistab self.id = rowID } + @objc public init( threadUniqueId: String, isArchived: Bool,