diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3c73dfa600..73d3543c70 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -16166,3 +16166,5 @@ Error: %8$@"; "Attachment.PublicMusic" = "PUBLIC CHATS"; "PeerInfo.UnofficialSecurityRisk" = "%@ uses an unofficial Telegram client – messages to this user may be less secure."; + +"Gallery.Live" = "LIVE"; diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index 8ec3af00ca..af98d0480d 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -128,6 +128,20 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest private var pagingEnabledDisposable: Disposable? private var edgeLongTapTimer: Foundation.Timer? + + private func isInteractiveControlView(_ view: UIView?) -> Bool { + var currentView = view + while let view = currentView { + if view is UIControl { + return true + } + if let _ = view.asyncdisplaykit_node as? ASButtonNode { + return true + } + currentView = view.superview + } + return false + } public init(pageGap: CGFloat, disableTapNavigation: Bool) { self.pageGap = pageGap @@ -220,7 +234,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest return .fail } - if let result = strongSelf.hitTest(point, with: nil), let _ = result.asyncdisplaykit_node as? ASButtonNode { + if strongSelf.isInteractiveControlView(strongSelf.hitTest(point, with: nil)) { return .fail } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 530f02b2f9..3077bd4ee4 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -38,6 +38,7 @@ import MultilineTextComponent import BundleIconComponent import VideoPlaybackControlsComponent import PhotoResources +import GlassBackgroundComponent public enum UniversalVideoGalleryItemContentInfo { case message(Message, GalleryMediaSubject?) @@ -893,7 +894,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let statusButtonNode: HighlightableButtonNode private let statusNode: RadialStatusNode private var statusNodeShouldBeHidden = true - private var livePhotoIconNode: ASImageNode? + private var livePhotoButton: LivePhotoButton? private let playbackControls = ComponentView() @@ -963,9 +964,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var activeEdgeRateIndicator: ComponentView? private var isAnimatingOut: Bool = false + private var isLivePhoto = false private var didAutoplayLivePhotoOnce = false - private var isPlayingLivePhotoByHolding = false + private var isLivePhotoPlaybackActive = false + private var isLivePhotoGestureActive = false init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context @@ -1275,6 +1278,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let transition = ComponentTransition(transition) transition.setFrame(view: playbackControlsView, frame: playbackControlsFrame) } + + if let livePhotoButton = self.livePhotoButton { + let livePhotoButtonSize = livePhotoButton.update(isPlaying: self.isLivePhotoPlaybackActive) + if livePhotoButton.superview == nil { + self.view.addSubview(livePhotoButton) + } + transition.updateFrame(view: livePhotoButton, frame: CGRect(origin: CGPoint(x: 16.0, y: max(navigationBarHeight, layout.statusBarHeight ?? 0.0) + 10.0), size: livePhotoButtonSize)) + } if let pictureInPictureNode = self.pictureInPictureNode { if let item = self.item { @@ -1374,10 +1385,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.statusButtonNode.isHidden = true } + self.resetLivePhotoPlayback() self.dismissOnOrientationChange = item.landscape self.isLivePhoto = false self.didAutoplayLivePhotoOnce = false - self.isPlayingLivePhotoByHolding = false + self.isLivePhotoPlaybackActive = false var hasLinkedStickers = false if let content = item.content as? NativeVideoContent { @@ -1716,7 +1728,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { initialBuffering = true } isPaused = !whilePlaying - if strongSelf.isPlayingLivePhotoByHolding { + if strongSelf.isLivePhotoPlaybackActive { isPaused = false } var isStreaming = false @@ -1961,7 +1973,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if strongSelf.isLivePhoto { - if !strongSelf.isPlayingLivePhotoByHolding { + if !strongSelf.isLivePhotoPlaybackActive { strongSelf.setLivePhotoVideoVisible(false, animated: true) } } else if let snapshotView = videoNode?.view.snapshotView(afterScreenUpdates: false) { @@ -1973,7 +1985,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if !strongSelf.isLivePhoto { videoNode?.seek(0.0) - } else if strongSelf.isPlayingLivePhotoByHolding { + } else if strongSelf.isLivePhotoPlaybackActive { videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: .loop) } @@ -2059,22 +2071,29 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if self.isLivePhoto { - let livePhotoIconNode: ASImageNode - if let current = self.livePhotoIconNode { - livePhotoIconNode = current + let livePhotoButton: LivePhotoButton + if let current = self.livePhotoButton { + livePhotoButton = current } else { - livePhotoIconNode = ASImageNode() - livePhotoIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/LiveOn"), color: .white) - //self.addSubnode(livePhotoIconNode) - self.livePhotoIconNode = livePhotoIconNode + livePhotoButton = LivePhotoButton(context: self.context) + livePhotoButton.pressed = { [weak self] in + guard let self else { + return + } + self.updateLivePhotoPlayback(isActive: !self.isLivePhotoPlaybackActive, animated: true) + } + self.livePhotoButton = livePhotoButton + self.view.addSubview(livePhotoButton) } - - if let icon = livePhotoIconNode.image { - livePhotoIconNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: icon.size) - } - } else if let livePhotoIconNode = self.livePhotoIconNode { - self.livePhotoIconNode = nil - livePhotoIconNode.removeFromSupernode() +// if livePhotoButton.superview == nil { +// +// } +// if let validLayout = self.validLayout { +// self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .immediate) +// } + } else if let livePhotoButton = self.livePhotoButton { + self.livePhotoButton = nil + livePhotoButton.removeFromSuperview() } } @@ -2169,24 +2188,29 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - private func updateLivePhotoHoldPlayback(isActive: Bool, animated: Bool) { - guard self.isLivePhoto, let videoNode = self.videoNode else { + private func updateLivePhotoPlayback(isActive: Bool, animated: Bool) { + guard self.isLivePhoto, self.isLivePhotoPlaybackActive != isActive, let videoNode = self.videoNode else { return } - guard self.isPlayingLivePhotoByHolding != isActive else { - return - } - - self.isPlayingLivePhotoByHolding = isActive + + self.isLivePhotoPlaybackActive = isActive if isActive { self.setLivePhotoVideoVisible(true, animated: animated) videoNode.seek(0.0) - videoNode.playOnceWithSound(playAndRecord: false) + videoNode.playOnceWithSound(playAndRecord: false, actionAtEnd: .loop) } else { videoNode.pause() videoNode.seek(0.0) self.setLivePhotoVideoVisible(false, animated: animated) } + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) + } + } + + private func resetLivePhotoPlayback() { + self.updateLivePhotoPlayback(isActive: false, animated: false) } override func centralityUpdated(isCentral: Bool) { @@ -2228,7 +2252,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } } else { - self.updateLivePhotoHoldPlayback(isActive: false, animated: false) + self.resetLivePhotoPlayback() self.isPlayingPromise.set(false) self.isPlaying = false @@ -2265,17 +2289,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if self.skipInitialPause { self.skipInitialPause = false } else { - self.updateLivePhotoHoldPlayback(isActive: false, animated: false) + self.resetLivePhotoPlayback() self.ignorePauseStatus = true videoNode.pause() videoNode.seek(0.0) } } else { - self.updateLivePhotoHoldPlayback(isActive: false, animated: false) + self.resetLivePhotoPlayback() if let status = self.playerStatusValue { self.maybeStorePlaybackStatus(status: status) } - videoNode.continuePlayingWithoutSound() + if !self.isLivePhoto { + videoNode.continuePlayingWithoutSound() + } } self.updateDisplayPlaceholder() } else if !item.fromPlayingVideo { @@ -2382,7 +2408,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd { if self.isLivePhoto { - return self.isPlayingLivePhotoByHolding ? .loop : .stop + return self.isLivePhotoPlaybackActive ? .loop : .stop } if let item = self.item { if !item.isSecret, let content = item.content as? NativeVideoContent, content.duration <= 30 { @@ -4082,7 +4108,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } if let edge, case .middle = edge { - self.updateLivePhotoHoldPlayback(isActive: true, animated: true) + guard self.isLivePhoto else { + return + } + if !self.isLivePhotoPlaybackActive { + self.updateLivePhotoPlayback(isActive: true, animated: true) + } + self.isLivePhotoGestureActive = true } else if let edge, case .right = edge { let effectiveRate: Double if let current = self.activeEdgeRateState { @@ -4097,7 +4129,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } videoNode.setBaseRate(effectiveRate) } else { - self.updateLivePhotoHoldPlayback(isActive: false, animated: true) + if self.isLivePhotoGestureActive { + self.updateLivePhotoPlayback(isActive: false, animated: true) + } + self.isLivePhotoGestureActive = false + if let (initialRate, _) = self.activeEdgeRateState { self.activeEdgeRateState = nil videoNode.setBaseRate(initialRate) @@ -4154,3 +4190,87 @@ final class HeaderContextReferenceContentSource: ContextReferenceContentSource { private func normalizeValue(_ value: CGFloat) -> CGFloat { return round(value * 10.0) / 10.0 } + +private final class LivePhotoButton: UIView { + private let context: AccountContext + + private let backgroundView: GlassBackgroundView + private let icon = ComponentView() + private let label = ComponentView() + private let button = HighlightTrackingButton() + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.bounds.insetBy(dx: -16.0, dy: -16.0).contains(point) + } + + var pressed: () -> Void = {} + + init(context: AccountContext) { + self.context = context + + self.backgroundView = GlassBackgroundView() + + super.init(frame: .zero) + + self.addSubview(self.backgroundView) + self.backgroundView.contentView.addSubview(self.button) + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + @objc private func buttonPressed() { + self.pressed() + } + + func update(isPlaying: Bool = false) -> CGSize { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + let iconName: String = isPlaying ? "Media Gallery/LivePhotoPlaying" : "Media Gallery/LivePhotoPlay" + let labelText: String = presentationData.strings.Gallery_Live + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent( + BundleIconComponent(name: iconName, tintColor: .white) + ), + environment: {}, + containerSize: CGSize(width: 18.0, height: 18.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: 8.0, y: floorToScreenPixels((30.0 - iconSize.height) / 2.0)), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(iconView) + } + iconView.frame = iconFrame + } + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent( + Text(text: labelText.uppercased(), font: Font.regular(12.0), color: .white) + ), + environment: {}, + containerSize: CGSize(width: 200.0, height: 18.0) + ) + let labelFrame = CGRect(origin: CGPoint(x: 28.0, y: floorToScreenPixels((30.0 - labelSize.height) / 2.0)), size: labelSize) + if let labelView = self.label.view { + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(labelView) + } + labelView.frame = labelFrame + } + + let size = CGSize(width: 21.0 + labelSize.width + 18.0, height: 30.0) + self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: true, tintColor: .init(kind: .panel), isInteractive: true, transition: .immediate) + self.backgroundView.frame = CGRect(origin: .zero, size: size) + self.button.frame = CGRect(origin: .zero, size: size).insetBy(dx: -16.0, dy: -16.0) + + return size + } +} diff --git a/submodules/MediaPickerUI/Sources/LivePhotoButton.swift b/submodules/MediaPickerUI/Sources/LivePhotoButton.swift index 6c6c552222..a6ac357d3d 100644 --- a/submodules/MediaPickerUI/Sources/LivePhotoButton.swift +++ b/submodules/MediaPickerUI/Sources/LivePhotoButton.swift @@ -217,7 +217,7 @@ final class LivePhotoButton: UIView, TGLivePhotoButton { let size = CGSize(width: 19.0 + labelSize.width + 16.0, height: 18.0) self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: true, tintColor: .init(kind: .panel), isInteractive: true, transition: .immediate) self.backgroundView.frame = CGRect(origin: .zero, size: size) - self.button.frame = CGRect(origin: .zero, size: size) + self.button.frame = CGRect(origin: .zero, size: size).insetBy(dx: -16.0, dy: -16.0) } } diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/Contents.json new file mode 100644 index 0000000000..b6dcf45856 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "play1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/play1.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/play1.pdf new file mode 100644 index 0000000000..cb363de894 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/play1.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlaying.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlaying.imageset/Contents.json new file mode 100644 index 0000000000..90125169c7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlaying.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "play2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlaying.imageset/play2.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlaying.imageset/play2.pdf new file mode 100644 index 0000000000..b67103f320 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlaying.imageset/play2.pdf differ