diff --git a/Signal/ConversationView/Components/CVComponentDateHeader.swift b/Signal/ConversationView/Components/CVComponentDateHeader.swift index 9fe827e5a0..3df9e5002e 100644 --- a/Signal/ConversationView/Components/CVComponentDateHeader.swift +++ b/Signal/ConversationView/Components/CVComponentDateHeader.swift @@ -144,11 +144,15 @@ public class CVComponentDateHeader: CVComponentBase, CVRootComponent { return contentView.rootView } + let isStandaloneRenderItem = conversationStyle.isStandaloneRenderItem + // On iOS 26 always use `visual effect` content view for the sticky header. if componentDelegate.isConversationPreview { return buildVisualEffectContentView() } else if hasWallpaper, #unavailable(iOS 26) { return buildPlainContentView() + } else if isStandaloneRenderItem { + return buildPlainContentView() } else { let plainContentView = buildPlainContentView() let visualEffectContentView = buildVisualEffectContentView() diff --git a/Signal/ConversationView/ConversationViewController+Banners.swift b/Signal/ConversationView/ConversationViewController+Banners.swift index 70d9531803..04074c7412 100644 --- a/Signal/ConversationView/ConversationViewController+Banners.swift +++ b/Signal/ConversationView/ConversationViewController+Banners.swift @@ -127,7 +127,7 @@ extension ConversationViewController { private class ConversationBannerView: UIView { internal var contentView: UIView & UIContentView - private var blurBackgroundView: UIVisualEffectView? + var blurBackgroundView: UIVisualEffectView? public static func fadeInAnimator() -> UIViewPropertyAnimator { return UIViewPropertyAnimator( @@ -1111,8 +1111,9 @@ internal extension ConversationViewController { ) let banner = ConversationBannerView(configuration: bannerConfiguration) + let longPressInteraction = UIContextMenuInteraction(delegate: self) - banner.addInteraction(longPressInteraction) + banner.blurBackgroundView?.addInteraction(longPressInteraction) // Set up interaction delegate for pin icon menu banner.pinnedMessageDelegate = self diff --git a/Signal/ConversationView/ConversationViewController+MessageActionsDelegate.swift b/Signal/ConversationView/ConversationViewController+MessageActionsDelegate.swift index 6081da9374..be9e0e987b 100644 --- a/Signal/ConversationView/ConversationViewController+MessageActionsDelegate.swift +++ b/Signal/ConversationView/ConversationViewController+MessageActionsDelegate.swift @@ -484,6 +484,30 @@ extension ConversationViewController: MessageActionsDelegate { } } + public func handleActionUnpinAsync(message: TSMessage) async { + let pinnedMessageManager = DependenciesBridge.shared.pinnedMessageManager + let db = DependenciesBridge.shared.db + + let unpinMessage = db.write { tx in + pinnedMessageManager.getOutgoingUnpinMessage( + interaction: message, + thread: thread, + expiresAt: nil, + tx: tx + ) + } + guard let unpinMessage else { + return + } + + await queuePinMessageChangeWithModal( + message: message, + pinMessage: unpinMessage, + completion: nil + ) + } + + func messageActionsChangePinStatus(_ itemViewModel: CVItemViewModelImpl, pin: Bool) { guard let message = itemViewModel.renderItem.interaction as? TSMessage else { return diff --git a/Signal/ConversationView/ConversationViewController+PinnedMessages.swift b/Signal/ConversationView/ConversationViewController+PinnedMessages.swift index c92998b10f..12b8d1578d 100644 --- a/Signal/ConversationView/ConversationViewController+PinnedMessages.swift +++ b/Signal/ConversationView/ConversationViewController+PinnedMessages.swift @@ -18,6 +18,9 @@ protocol PinnedMessageInteractionManagerDelegate: AnyObject { /// Presents the "see all messages" details view. func presentSeeAllMessages() + + /// Unpins all messages + func unpinAllMessages() } public struct PinnedMessageBannerData { @@ -355,4 +358,18 @@ extension ConversationViewController: PinnedMessageInteractionManagerDelegate { pmDetailsController.modalPresentationStyle = .pageSheet present(pmDetailsController, animated: true) } + + func unpinAllMessages() { + Task { + for message in threadViewModel.pinnedMessages { + await handleActionUnpinAsync(message: message) + } + presentToast( + text: OWSLocalizedString( + "PINNED_MESSAGE_TOAST", + comment: "Text to show on a toast when someone unpins a message" + ) + ) + } + } } diff --git a/Signal/src/ViewControllers/PinnedMessages/PinnedMessagesDetailsViewController.swift b/Signal/src/ViewControllers/PinnedMessages/PinnedMessagesDetailsViewController.swift index 07bd150826..4e872dc186 100644 --- a/Signal/src/ViewControllers/PinnedMessages/PinnedMessagesDetailsViewController.swift +++ b/Signal/src/ViewControllers/PinnedMessages/PinnedMessagesDetailsViewController.swift @@ -57,6 +57,7 @@ class PinnedMessagesDetailsViewController: OWSViewController, DatabaseChangeDele titleStackView.spacing = 4 navigationItem.titleView = titleStackView + navigationItem.rightBarButtonItem = .doneButton(dismissingFrom: self) } private func layoutPinnedMessages(tx: DBReadTransaction) { @@ -112,7 +113,8 @@ class PinnedMessagesDetailsViewController: OWSViewController, DatabaseChangeDele scrollView.autoPinEdgesToSuperviewEdges() paddedContainerView.autoPinEdgesToSuperviewEdges() - stack.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)) + + stack.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 16, left: 16, bottom: 64, right: 16)) paddedContainerView.autoMatch(.width, to: .width, of: scrollView) } @@ -134,6 +136,23 @@ class PinnedMessagesDetailsViewController: OWSViewController, DatabaseChangeDele db.read { tx in layoutPinnedMessages(tx: tx) } + + let unpinAllButton = UIButton( + configuration: .largeSecondary(title: OWSLocalizedString( + "PINNED_MESSAGES_UNPIN_ALL", + comment: "Title for a button to unpin all pinned messages." + )), + primaryAction: UIAction { [weak self] _ in + self?.dismiss(animated: true) + self?.delegate?.unpinAllMessages() + }, + ) + view.addSubview(unpinAllButton) + unpinAllButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + unpinAllButton.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor), + unpinAllButton.centerXAnchor.constraint(equalTo: contentLayoutGuide.centerXAnchor), + ]) } private func buildButtonAndCellStack(renderItem: CVRenderItem, message: TSMessage, reversedIndex: Int) -> UIStackView { @@ -192,7 +211,8 @@ class PinnedMessagesDetailsViewController: OWSViewController, DatabaseChangeDele chatColor: DependenciesBridge.shared.chatColorSettingStore.resolvedChatColor( for: threadViewModel.threadRecord, tx: tx - ) + ), + isStandaloneRenderItem: true ) return CVLoader.buildStandaloneRenderItem( @@ -521,7 +541,7 @@ extension PinnedMessagesDetailsViewController: CVComponentDelegate { return {} } - var isConversationPreview: Bool { true } + var isConversationPreview: Bool { false } var wallpaperBlurProvider: WallpaperBlurProvider? { nil } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 73dc39bf09..12d1aa51e9 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -6616,6 +6616,9 @@ /* Action menu item to unpin a message */ "PINNED_MESSAGES_UNPIN" = "Unpin"; +/* Title for a button to unpin all pinned messages. */ +"PINNED_MESSAGES_UNPIN_ALL" = "Unpin All"; + /* The title for pinned conversation section on the conversation list */ "PINNED_SECTION_TITLE" = "Pinned"; diff --git a/SignalUI/Appearance/ConversationStyle.swift b/SignalUI/Appearance/ConversationStyle.swift index c822c4231e..ee2dee8395 100644 --- a/SignalUI/Appearance/ConversationStyle.swift +++ b/SignalUI/Appearance/ConversationStyle.swift @@ -38,6 +38,8 @@ public struct ConversationStyle { public let isWallpaperPhoto: Bool + public let isStandaloneRenderItem: Bool + private let dynamicBodyTypePointSize: CGFloat private let primaryTextColor: UIColor @@ -109,7 +111,8 @@ public struct ConversationStyle { viewWidth: CGFloat, hasWallpaper: Bool, isWallpaperPhoto: Bool, - chatColor: ColorOrGradientSetting + chatColor: ColorOrGradientSetting, + isStandaloneRenderItem: Bool = false ) { self.type = type self.viewWidth = viewWidth @@ -169,6 +172,8 @@ public struct ConversationStyle { let kMaxAudioMessageWidth: CGFloat = 244 maxAudioMessageWidth = floor(min(maxMessageWidth, kMaxAudioMessageWidth)) + + self.isStandaloneRenderItem = isStandaloneRenderItem } // MARK: Colors