From ca3eacda45a45806bdd0b998d8121b17a1b73e71 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 30 Mar 2026 04:37:05 +0200 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../GalleryUI/Sources/GalleryPagerNode.swift | 16 +- .../Items/UniversalVideoGalleryItem.swift | 190 ++++++++++++++---- .../Sources/LivePhotoButton.swift | 2 +- .../LivePhotoPlay.imageset/Contents.json | 12 ++ .../LivePhotoPlay.imageset/play1.pdf | Bin 0 -> 9018 bytes .../LivePhotoPlaying.imageset/Contents.json | 12 ++ .../LivePhotoPlaying.imageset/play2.pdf | Bin 0 -> 9395 bytes 8 files changed, 197 insertions(+), 37 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/play1.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlaying.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlaying.imageset/play2.pdf 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 0000000000000000000000000000000000000000..cb363de8949db5656fbd89eeee221400a73552dd GIT binary patch literal 9018 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 HSN(qgq#8%Y literal 0 HcmV?d00001