diff --git a/Signal/ConversationView/CVItemViewModelImpl.swift b/Signal/ConversationView/CVItemViewModelImpl.swift index 68a0d83bcf7..4c97813205f 100644 --- a/Signal/ConversationView/CVItemViewModelImpl.swift +++ b/Signal/ConversationView/CVItemViewModelImpl.swift @@ -236,6 +236,10 @@ extension CVItemViewModelImpl { AttachmentSharing.showShareUI(for: attachments, sender: sender) } + var isSticker: Bool { + return messageCellType == .stickerMessage + } + var canForwardMessage: Bool { guard !isViewOnce else { return false diff --git a/Signal/ConversationView/CellViews/CVMediaView.swift b/Signal/ConversationView/CellViews/CVMediaView.swift index 3131f237411..5ca3fac0d6d 100644 --- a/Signal/ConversationView/CellViews/CVMediaView.swift +++ b/Signal/ConversationView/CellViews/CVMediaView.swift @@ -155,7 +155,7 @@ public class CVMediaView: ManualLayoutViewWithLayer { mediaView.backgroundColor = isBorderless ? .clear : Theme.washColor if !addProgressIfNecessary() { - if reusableMediaView.isVideo { + if reusableMediaView.needsPlayButton { addVideoPlayButton() } } diff --git a/Signal/ConversationView/CellViews/ReusableMediaView.swift b/Signal/ConversationView/CellViews/ReusableMediaView.swift index b0a89408eea..32f41fa9d41 100644 --- a/Signal/ConversationView/CellViews/ReusableMediaView.swift +++ b/Signal/ConversationView/CellViews/ReusableMediaView.swift @@ -65,6 +65,21 @@ public class ReusableMediaView: NSObject { mediaViewAdapter is MediaViewAdapterVideo } + var needsPlayButton: Bool { + mediaViewAdapter is MediaViewAdapterVideo + || ( + UIAccessibility.isReduceMotionEnabled + && ( + mediaViewAdapter is MediaViewAdapterLoopingVideo + || ( + mediaViewAdapter is MediaViewAdapterSticker + && mediaViewAdapter.shouldBeRenderedByYY + ) + || mediaViewAdapter is MediaViewAdapterAnimated + ) + ) + } + // MARK: - LoadState // Thread-safe access to load state. @@ -332,6 +347,7 @@ class MediaViewAdapterAnimated: MediaViewAdapterSwift { return } imageView.image = image + imageView.autoPlayAnimatedImage = !UIAccessibility.isReduceMotionEnabled } func unloadMedia() { @@ -568,6 +584,9 @@ public class MediaViewAdapterSticker: NSObject, MediaViewAdapterSwift { owsFailDebug("Media has unexpected type: \(type(of: media))") return } + if let yyView = imageView as? CVAnimatedImageView { + yyView.autoPlayAnimatedImage = !UIAccessibility.isReduceMotionEnabled + } imageView.image = image } else { guard let image = media as? UIImage else { diff --git a/Signal/ConversationView/Components/CVComponentSticker.swift b/Signal/ConversationView/Components/CVComponentSticker.swift index abb2c47a44f..cef503d9480 100644 --- a/Signal/ConversationView/Components/CVComponentSticker.swift +++ b/Signal/ConversationView/Components/CVComponentSticker.swift @@ -158,6 +158,18 @@ public class CVComponentSticker: CVComponentBase, CVComponent { // MARK: - Events + private func toggleStickerAnimation(_ view: CVComponentView) { + if let stickerView = view as? CVComponentViewSticker, let rmv = stickerView.reusableMediaView, let yyView = rmv.mediaView as? CVAnimatedImageView { + if yyView.isAnimating { + yyView.stopAnimating() + stickerView.togglePlayButton() + } else { + stickerView.togglePlayButton() + yyView.startAnimating() + } + } + } + public override func handleTap(sender: UIGestureRecognizer, componentDelegate: CVComponentDelegate, componentView: CVComponentView, @@ -168,7 +180,15 @@ public class CVComponentSticker: CVComponentBase, CVComponent { // Not yet downloaded. return false } - componentDelegate.didTapStickerPack(stickerMetadata.packInfo) + var isAnimated = false + if let stickerComponent = componentView as? CVComponentViewSticker { + isAnimated = stickerComponent.isAnimated + } + if UIAccessibility.isReduceMotionEnabled && isAnimated { + toggleStickerAnimation(componentView) + } else { + componentDelegate.didTapStickerPack(stickerMetadata.packInfo) + } return true } @@ -179,6 +199,7 @@ public class CVComponentSticker: CVComponentBase, CVComponent { public class CVComponentViewSticker: NSObject, CVComponentView { fileprivate let stackView = ManualStackView(name: "sticker.container") + fileprivate var playButtonView: UIView? = nil fileprivate var reusableMediaView: ReusableMediaView? @@ -188,11 +209,21 @@ public class CVComponentSticker: CVComponentBase, CVComponent { stackView } + public var isAnimated: Bool { + get { + reusableMediaView?.needsPlayButton != nil && (reusableMediaView?.needsPlayButton)! || false + } + } + public func setIsCellVisible(_ isCellVisible: Bool) { if isCellVisible { - if let reusableMediaView = reusableMediaView, - reusableMediaView.owner == self { - reusableMediaView.load() + if let reusableMediaView = reusableMediaView { + if reusableMediaView.owner == self { + reusableMediaView.load() + } + if reusableMediaView.needsPlayButton { + addPlayButton() + } } } else { if let reusableMediaView = reusableMediaView, @@ -202,6 +233,36 @@ public class CVComponentSticker: CVComponentBase, CVComponent { } } + private func addPlayButton() { + if playButtonView != nil { + return + } + let playButtonWidth: CGFloat = 44 + let playIconWidth: CGFloat = 20 + + let playButton = UIView.transparentContainer() + playButtonView = playButton + stackView.addSubviewToCenterOnSuperview(playButton, size: CGSize(square: playButtonWidth)) + + let playCircleView = OWSLayerView.circleView() + playCircleView.backgroundColor = UIColor.ows_black.withAlphaComponent(0.7) + playCircleView.isUserInteractionEnabled = false + playButton.addSubview(playCircleView) + stackView.layoutSubviewToFillSuperviewEdges(playCircleView) + + let playIconView = CVImageView() + playIconView.setTemplateImageName("play-fill-32", tintColor: UIColor.ows_white) + playIconView.isUserInteractionEnabled = false + stackView.addSubviewToCenterOnSuperview(playIconView, + size: CGSize(square: playIconWidth)) + } + + fileprivate func togglePlayButton() { + if let playButton = playButtonView { + playButton.isHidden = !playButton.isHidden + } + } + public func reset() { stackView.reset() diff --git a/Signal/ConversationView/ConversationViewController+MessageActions.swift b/Signal/ConversationView/ConversationViewController+MessageActions.swift index 15e1b5e380b..6d2ba7b08c6 100644 --- a/Signal/ConversationView/ConversationViewController+MessageActions.swift +++ b/Signal/ConversationView/ConversationViewController+MessageActions.swift @@ -122,6 +122,7 @@ extension ConversationViewController: ContextMenuInteractionDelegate { .showPaymentDetails, .speak, .stopSpeaking, + .showStickerPack, .info, .delete ] diff --git a/Signal/ConversationView/ConversationViewController+MessageActionsDelegate.swift b/Signal/ConversationView/ConversationViewController+MessageActionsDelegate.swift index de4a9952e30..48626b20c9f 100644 --- a/Signal/ConversationView/ConversationViewController+MessageActionsDelegate.swift +++ b/Signal/ConversationView/ConversationViewController+MessageActionsDelegate.swift @@ -278,4 +278,10 @@ extension ConversationViewController: MessageActionsDelegate { let paymentsDetailViewController = PaymentsDetailViewController(paymentItem: paymentHistoryItem) navigationController?.pushViewController(paymentsDetailViewController, animated: true) } + + func messageActionsShowStickerPack(_ itemViewModel: CVItemViewModelImpl) { + if let stickerMetadata = itemViewModel.stickerMetadata { + didTapStickerPack(stickerMetadata.packInfo) + } + } } diff --git a/Signal/ConversationView/MessageActions.swift b/Signal/ConversationView/MessageActions.swift index 1752b41e9ac..2753b832e40 100644 --- a/Signal/ConversationView/MessageActions.swift +++ b/Signal/ConversationView/MessageActions.swift @@ -15,6 +15,7 @@ protocol MessageActionsDelegate: AnyObject { func messageActionsStopSpeakingItem(_ itemViewModel: CVItemViewModelImpl) func messageActionsEditItem(_ itemViewModel: CVItemViewModelImpl) func messageActionsShowPaymentDetails(_ itemViewModel: CVItemViewModelImpl) + func messageActionsShowStickerPack(_ itemViewModel: CVItemViewModelImpl) } // MARK: - @@ -151,6 +152,19 @@ struct MessageActionBuilder { } ) } + + static func showStickerPack(itemViewModel: CVItemViewModelImpl, delegate: MessageActionsDelegate) -> MessageAction { + MessageAction( + .showStickerPack, + accessibilityLabel: OWSLocalizedString("MESSAGE_ACTION_SHOW_STICKER_PACK", comment: "Action sheet accessibility label"), + accessibilityIdentifier: UIView.accessibilityIdentifier(containerName: "message_action", name: "show_sticker_pack"), + contextMenuTitle: OWSLocalizedString("CONTEXT_MENU_SHOW_STICKER_PACK", comment: "Context menu button title"), + contextMenuAttributes: [], + block: { [weak delegate] _ in + delegate?.messageActionsShowStickerPack(itemViewModel) + } + ) + } } class MessageActions: NSObject { @@ -229,6 +243,11 @@ class MessageActions: NSObject { actions.append(editAction) } + if itemViewModel.isSticker { + let showPackAction = MessageActionBuilder.showStickerPack(itemViewModel: itemViewModel, delegate: delegate) + actions.append(showPackAction) + } + let selectAction = MessageActionBuilder.selectMessage(itemViewModel: itemViewModel, delegate: delegate) actions.append(selectAction) diff --git a/Signal/src/ViewControllers/MediaGallery/MediaItemViewController.swift b/Signal/src/ViewControllers/MediaGallery/MediaItemViewController.swift index de7e59bb45f..7e4f57fcea7 100644 --- a/Signal/src/ViewControllers/MediaGallery/MediaItemViewController.swift +++ b/Signal/src/ViewControllers/MediaGallery/MediaItemViewController.swift @@ -206,7 +206,7 @@ class MediaItemViewController: OWSViewController, VideoPlaybackStatusProvider { guard mediaView == nil else { return } let view: UIView - if attachmentStream.contentType.isVideo, galleryItem.renderingFlag == .shouldLoop { + if attachmentStream.contentType.isVideo, galleryItem.renderingFlag == .shouldLoop, !UIAccessibility.isReduceMotionEnabled { if attachmentStream.contentType.isVideo, let loopingVideoPlayerView = buildLoopingVideoPlayerView() { loopingVideoPlayerView.delegate = self view = loopingVideoPlayerView @@ -331,7 +331,7 @@ class MediaItemViewController: OWSViewController, VideoPlaybackStatusProvider { private var hasAutoPlayedVideo = false private var isVideo: Bool { - galleryItem.isVideo + galleryItem.isVideo || (UIAccessibility.isReduceMotionEnabled && galleryItem.isAnimated) } private func playVideo() { diff --git a/Signal/src/ViewControllers/MessageActionsToolbar.swift b/Signal/src/ViewControllers/MessageActionsToolbar.swift index 9a5e8318be7..abeae7ee0d9 100644 --- a/Signal/src/ViewControllers/MessageActionsToolbar.swift +++ b/Signal/src/ViewControllers/MessageActionsToolbar.swift @@ -26,6 +26,7 @@ public class MessageAction: NSObject { case stopSpeaking case edit case showPaymentDetails + case showStickerPack } let actionType: MessageActionType @@ -70,6 +71,8 @@ public class MessageAction: NSObject { return .contextMenuEdit case .showPaymentDetails: return .settingsPayments + case .showStickerPack: + return .contextMenuSticker } }() return Theme.iconImage(icon) diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index c52ab894787..71c50046460 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1471,6 +1471,9 @@ /* Context menu button title */ "CONTEXT_MENU_SHARE_MEDIA" = "Share"; +/* Context menu button title */ +"CONTEXT_MENU_SHOW_STICKER_PACK" = "Show Sticker Pack"; + /* Context menu button title */ "CONTEXT_MENU_SPEAK_MESSAGE" = "Speak"; @@ -4414,6 +4417,9 @@ /* Action sheet button title */ "MESSAGE_ACTION_SHARE_MEDIA" = "Share Media"; +/* Aciton sheet button title */ +"MESSAGE_ACTION_SHOW_STICKER_PACK" = "Show Sticker Pack"; + /* Action sheet accessibility label */ "MESSAGE_ACTION_SPEAK_MESSAGE" = "Speak Message"; diff --git a/SignalUI/Appearance/Theme+Icons.swift b/SignalUI/Appearance/Theme+Icons.swift index 828ea0fa396..5f4a88f7837 100644 --- a/SignalUI/Appearance/Theme+Icons.swift +++ b/SignalUI/Appearance/Theme+Icons.swift @@ -127,6 +127,7 @@ public enum ThemeIcon: UInt { case contextMenuVoiceCall case contextMenuVideoCall case contextMenuMessage + case contextMenuSticker case composeNewGroupLarge case composeFindByUsernameLarge @@ -447,6 +448,8 @@ public extension Theme { return "video-light" case .contextMenuMessage: return "chat-light" + case .contextMenuSticker: + return "sticker" // Empty chat list case .composeNewGroupLarge: diff --git a/SignalUI/Stickers/StickerView.swift b/SignalUI/Stickers/StickerView.swift index 77862183b2b..4d3ca8bd8dc 100644 --- a/SignalUI/Stickers/StickerView.swift +++ b/SignalUI/Stickers/StickerView.swift @@ -78,6 +78,7 @@ public class StickerView { let yyView = YYAnimatedImageView() yyView.alwaysInfiniteLoop = true yyView.contentMode = .scaleAspectFit + yyView.autoPlayAnimatedImage = !UIAccessibility.isReduceMotionEnabled yyView.image = stickerImage stickerView = yyView } diff --git a/SignalUI/Views/LoopingVideoView.swift b/SignalUI/Views/LoopingVideoView.swift index 6e633706e12..2e35c4e7a7d 100644 --- a/SignalUI/Views/LoopingVideoView.swift +++ b/SignalUI/Views/LoopingVideoView.swift @@ -142,7 +142,9 @@ public class LoopingVideoView: UIView { if let asset = video?.asset { let playerItem = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: ["tracks"]) self.player.replaceCurrentItem(with: playerItem) - self.player.play() + if !UIAccessibility.isReduceMotionEnabled { + self.player.play() + } self.invalidateIntrinsicContentSize() self.delegate?.loopingVideoViewChangedPlayerItem() }