diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index 1a2a01d2af..909a81bfcd 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -104,8 +104,9 @@ private final class InnerActionsContainerNode: ASDisplayNode { } } case let .custom(item, _): - itemNodes.append(.custom(item.node(presentationData: presentationData, getController: getController, actionSelected: actionSelected))) - if i != items.count - 1 { + let itemNode = item.node(presentationData: presentationData, getController: getController, actionSelected: actionSelected) + itemNodes.append(.custom(itemNode)) + if i != items.count - 1 && itemNode.needsSeparator { switch items[i + 1] { case .action, .custom: let separatorNode = ASDisplayNode() diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 3209430d8a..0a39323617 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -226,6 +226,14 @@ public protocol ContextMenuCustomNode: ASDisplayNode { func canBeHighlighted() -> Bool func updateIsHighlighted(isHighlighted: Bool) func performAction() + + var needsSeparator: Bool { get } +} + +public extension ContextMenuCustomNode { + var needsSeparator: Bool { + return true + } } public protocol ContextMenuCustomItem { diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index c1feba5545..71efef132d 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -629,7 +629,7 @@ private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, C private let requestDismiss: (ContextMenuActionResult) -> Void private var presentationData: PresentationData? - private var itemNode: ContextMenuCustomNode? + private(set) var itemNode: ContextMenuCustomNode? init( getController: @escaping () -> ContextControllerProtocol?, @@ -862,18 +862,28 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio if let separatorNode = item.separatorNode { itemTransition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.maxY), size: CGSize(width: itemFrame.width, height: UIScreenPixel)), beginWithCurrentState: true) + + var separatorHidden = false if i != self.itemNodes.count - 1 { switch self.items[i + 1] { case .separator: - separatorNode.isHidden = true + separatorHidden = true case .action: - separatorNode.isHidden = false + separatorHidden = false case .custom: - separatorNode.isHidden = false + separatorHidden = false } } else { - separatorNode.isHidden = true + separatorHidden = true } + + if let itemContainerNode = item.node as? ContextControllerActionsListCustomItemNode, let itemNode = itemContainerNode.itemNode { + if !itemNode.needsSeparator { + separatorHidden = true + } + } + + separatorNode.isHidden = separatorHidden } itemNodeLayout.apply(itemSize, itemTransition) diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index fbc1c91a09..19fa027e58 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -45,7 +45,8 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", - "//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem", + "//submodules/TelegramUI/Components/SliderContextItem", + "//submodules/TelegramUI/Components/SectionTitleContextItem", "//submodules/TooltipUI", "//submodules/TelegramNotices", "//submodules/Pasteboard", diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index f8683a76b6..c43b65be67 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -28,6 +28,7 @@ import AdUI import AdsInfoScreen import AdsReportScreen import SaveProgressScreen +import SectionTitleContextItem public enum UniversalVideoGalleryItemContentInfo { case message(Message, Int?) @@ -503,6 +504,111 @@ final class MoreHeaderButton: HighlightableButtonNode { } } +final class SettingsHeaderButton: HighlightableButtonNode { + let referenceNode: ContextReferenceContentNode + let containerNode: ContextControllerSourceNode + private let iconNode: ASImageNode + private let iconDotNode: ASImageNode + + private var isMenuOpen: Bool = false + + var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + + private let wide: Bool + + init(wide: Bool = false) { + self.wide = wide + + self.referenceNode = ContextReferenceContentNode() + self.containerNode = ContextControllerSourceNode() + self.containerNode.animateScale = false + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.contentMode = .scaleToFill + + self.iconDotNode = ASImageNode() + self.iconDotNode.displaysAsynchronously = false + self.iconDotNode.displayWithoutProcessing = true + + super.init() + + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsNoDot"), color: .white) + self.iconDotNode.image = generateFilledCircleImage(diameter: 4.0, color: .white) + + self.containerNode.addSubnode(self.referenceNode) + self.referenceNode.addSubnode(self.iconNode) + self.referenceNode.addSubnode(self.iconDotNode) + self.addSubnode(self.containerNode) + + self.containerNode.shouldBegin = { [weak self] location in + guard let strongSelf = self, let _ = strongSelf.contextAction else { + return false + } + return true + } + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self else { + return + } + strongSelf.contextAction?(strongSelf.containerNode, gesture) + } + + self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 44.0)) + self.referenceNode.frame = self.containerNode.bounds + + self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -4.0, bottom: 0.0, right: -4.0) + + if let image = self.iconNode.image { + let iconFrame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) + self.iconNode.position = iconFrame.center + self.iconNode.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + if let dotImage = self.iconDotNode.image { + let dotFrame = CGRect(origin: CGPoint(x: iconFrame.minX + floorToScreenPixels((iconFrame.width - dotImage.size.width) * 0.5), y: iconFrame.minY + floorToScreenPixels((iconFrame.height - dotImage.size.height) * 0.5)), size: dotImage.size) + self.iconDotNode.position = dotFrame.center + self.iconDotNode.bounds = CGRect(origin: CGPoint(), size: dotFrame.size) + } + } + } + + override func didLoad() { + super.didLoad() + self.view.isOpaque = false + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: wide ? 32.0 : 22.0, height: 44.0) + } + + func onLayout() { + } + + func setIsMenuOpen(isMenuOpen: Bool) { + if self.isMenuOpen == isMenuOpen { + return + } + self.isMenuOpen = isMenuOpen + + let rotationTransition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + rotationTransition.updateTransform(node: self.iconNode, transform: CGAffineTransformMakeRotation(isMenuOpen ? (CGFloat.pi * 2.0 / 6.0) : 0.0)) + self.iconNode.layer.animateScale(from: 1.0, to: 1.07, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.iconNode.layer.animateScale(from: 1.07, to: 1.0, duration: 0.1, removeOnCompletion: false) + }) + + self.iconDotNode.layer.animateScale(from: 1.0, to: 0.8, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.iconDotNode.layer.animateScale(from: 0.8, to: 1.0, duration: 0.1, removeOnCompletion: false) + }) + } +} + @available(iOS 15.0, *) private final class PictureInPictureContentImpl: NSObject, PictureInPictureContent, AVPictureInPictureControllerDelegate { private final class PlaybackDelegate: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate { @@ -1062,7 +1168,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var moreBarButtonRate: Double = 1.0 private var moreBarButtonRateTimestamp: Double? - private let settingsBarButton: MoreHeaderButton + private let settingsBarButton: SettingsHeaderButton private var videoNode: UniversalVideoNode? private var videoNodeUserInteractionEnabled: Bool = false @@ -1098,6 +1204,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let statusDisposable = MetaDisposable() private let moreButtonStateDisposable = MetaDisposable() + private let settingsButtonStateDisposable = MetaDisposable() private let mediaPlaybackStateDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable() @@ -1113,6 +1220,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let isInteractingPromise = ValuePromise(false, ignoreRepeated: true) private let controlsVisiblePromise = ValuePromise(true, ignoreRepeated: true) private let isShowingContextMenuPromise = ValuePromise(false, ignoreRepeated: true) + private let isShowingSettingsMenuPromise = ValuePromise(false, ignoreRepeated: true) private let hasExpandedCaptionPromise = Promise() private var hideControlsDisposable: Disposable? private var automaticPictureInPictureDisposable: Disposable? @@ -1150,7 +1258,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.moreBarButton.isUserInteractionEnabled = true self.moreBarButton.setContent(.more(optionsCircleImage(dark: false))) - self.settingsBarButton = MoreHeaderButton() + self.settingsBarButton = SettingsHeaderButton() self.settingsBarButton.isUserInteractionEnabled = true super.init() @@ -1282,9 +1390,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.titleContentView = GalleryTitleView(frame: CGRect()) self._titleView.set(.single(self.titleContentView)) - let shouldHideControlsSignal: Signal = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get(), self.hasExpandedCaptionPromise.get()) - |> mapToSignal { isPlaying, isInteracting, controlsVisible, isShowingContextMenu, hasExpandedCaptionPromise -> Signal in - if isShowingContextMenu || hasExpandedCaptionPromise { + let shouldHideControlsSignal: Signal = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get(), self.isShowingSettingsMenuPromise.get(), self.hasExpandedCaptionPromise.get()) + |> mapToSignal { isPlaying, isInteracting, controlsVisible, isShowingContextMenu, isShowingSettingsMenu, hasExpandedCaptionPromise -> Signal in + if isShowingContextMenu || isShowingSettingsMenu || hasExpandedCaptionPromise { return .complete() } if isPlaying && !isInteracting && controlsVisible { @@ -1306,6 +1414,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { deinit { self.statusDisposable.dispose() self.moreButtonStateDisposable.dispose() + self.settingsButtonStateDisposable.dispose() self.mediaPlaybackStateDisposable.dispose() self.scrubbingFrameDisposable?.dispose() self.hideControlsDisposable?.dispose() @@ -1453,11 +1562,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { isAdaptive = true } - if isAdaptive { - self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white))) - } else { - self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettings"), color: .white))) - } + //TODO:release + //self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white))) + let _ = isAdaptive let dimensions = item.content.dimensions if dimensions.height > 0.0 { @@ -1638,6 +1745,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } }))*/ + + self.settingsButtonStateDisposable.set((self.isShowingSettingsMenuPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] isShowingSettingsMenu in + guard let self else { + return + } + self.settingsBarButton.setIsMenuOpen(isMenuOpen: isShowingSettingsMenu) + })) self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus) |> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in @@ -2951,25 +3066,42 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { guard let controller = self.baseNavigationController()?.topViewController as? ViewController else { return } + var dismissImpl: (() -> Void)? - let items: Signal<[ContextMenuItem], NoError> + let items: Signal<(items: [ContextMenuItem], topItems: [ContextMenuItem]), NoError> if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute { - items = self.adMenuMainItems() + items = self.adMenuMainItems() |> map { items in + return (items, []) + } } else { items = self.contextMenuMainItems(isSettings: isSettings, dismiss: { dismissImpl?() }) } - let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) - self.isShowingContextMenuPromise.set(true) + let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { items in + if !items.topItems.isEmpty { + return ContextController.Items(content: .twoLists(items.items, items.topItems)) + } else { + return ContextController.Items(content: .list(items.items)) + } + }, gesture: gesture) + if isSettings { + self.isShowingSettingsMenuPromise.set(true) + } else { + self.isShowingContextMenuPromise.set(true) + } controller.presentInGlobalOverlay(contextController) dismissImpl = { [weak contextController] in contextController?.dismiss() } contextController.dismissed = { [weak self] in - Queue.mainQueue().after(0.1, { - self?.isShowingContextMenuPromise.set(false) + Queue.mainQueue().after(isSettings ? 0.0 : 0.1, { + if isSettings { + self?.isShowingSettingsMenuPromise.set(false) + } else { + self?.isShowingContextMenuPromise.set(false) + } }) } } @@ -3112,9 +3244,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } - private func contextMenuMainItems(isSettings: Bool, dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> { + private func contextMenuMainItems(isSettings: Bool, dismiss: @escaping () -> Void) -> Signal<(items: [ContextMenuItem], topItems: [ContextMenuItem]), NoError> { guard let videoNode = self.videoNode, let item = self.item else { - return .single([]) + return .single(([], [])) } let peer: Signal @@ -3126,129 +3258,131 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return combineLatest(queue: Queue.mainQueue(), videoNode.status, peer) |> take(1) - |> map { [weak self] status, peer -> [ContextMenuItem] in + |> map { [weak self] status, peer -> (items: [ContextMenuItem], topItems: [ContextMenuItem]) in guard let status = status, let strongSelf = self else { - return [] + return ([], []) } + var topItems: [ContextMenuItem] = [] var items: [ContextMenuItem] = [] if isSettings { - var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal - var speedIconText: String = "1x" - var didSetSpeedValue = false - for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) { - if abs(speed - status.baseRate) < 0.01 { - speedValue = text - speedIconText = iconText - didSetSpeedValue = true - break - } - } - if !didSetSpeedValue && status.baseRate != 1.0 { - speedValue = String(format: "%.1fx", status.baseRate) - speedIconText = speedValue - } - - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in - return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor) - }, action: { c, _ in - guard let strongSelf = self else { - c?.dismiss(completion: nil) + let sliderValuePromise = ValuePromise(nil) + topItems.append(.custom(SliderContextItem(title: "Speed", minValue: 0.2, maxValue: 2.5, value: status.baseRate, valueChanged: { [weak self] newValue, _ in + guard let strongSelf = self, let videoNode = strongSelf.videoNode else { return } - - c?.pushItems(items: strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }) - }))) + let newValue = normalizeValue(newValue) + videoNode.setBaseRate(newValue) + if let controller = strongSelf.galleryController() as? GalleryController { + controller.updateSharedPlaybackRate(newValue) + } + sliderValuePromise.set(newValue) + }), true)) if let videoQualityState = strongSelf.videoNode?.videoQualityState(), !videoQualityState.available.isEmpty { - items.append(.separator) + } else { + items.append(.custom(SectionTitleContextItem(text: "PLAYBACK SPEED"), false)) + for (text, _, rate) in strongSelf.speedList(strings: strongSelf.presentationData.strings) { + let isSelected = abs(status.baseRate - rate) < 0.01 + items.append(.action(ContextMenuActionItem(text: text, icon: { _ in return nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), signal: sliderValuePromise.get() + |> map { value in + if isSelected && value == nil { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }), action: { _, f in + f(.default) + + guard let strongSelf = self, let videoNode = strongSelf.videoNode else { + return + } + + videoNode.setBaseRate(rate) + if let controller = strongSelf.galleryController() as? GalleryController { + controller.updateSharedPlaybackRate(rate) + } + }))) + } + } + + if let videoQualityState = strongSelf.videoNode?.videoQualityState(), !videoQualityState.available.isEmpty { + items.append(.custom(SectionTitleContextItem(text: "VIDEO QUALITY"), false)) //TODO:localize - - let qualityText: String - switch videoQualityState.preferred { - case .auto: + do { + let isSelected = videoQualityState.preferred == .auto + let qualityText: String = "Auto" + let textLayout: ContextMenuActionItemTextLayout if videoQualityState.current != 0 { - qualityText = "Auto (\(videoQualityState.current)p)" + textLayout = .secondLineWithValue("\(videoQualityState.current)p") } else { - qualityText = "Auto" + textLayout = .singleLine } - case let .quality(value): - qualityText = "\(value)p" + items.append(.action(ContextMenuActionItem(text: qualityText, textLayout: textLayout, icon: { _ in + if isSelected { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + + guard let self, let videoNode = self.videoNode else { + return + } + videoNode.setVideoQuality(.auto) + //TODO:release + //self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white))) + + /*if let controller = strongSelf.galleryController() as? GalleryController { + controller.updateSharedPlaybackRate(rate) + }*/ + }))) } - items.append(.action(ContextMenuActionItem(text: "Video Quality", textLayout: .secondLineWithValue(qualityText), icon: { _ in - return nil - }, action: { c, _ in - guard let strongSelf = self else { - c?.dismiss(completion: nil) - return + for quality in videoQualityState.available { + let isSelected = videoQualityState.preferred == .quality(quality) + let qualityTitle: String + if quality <= 360 { + qualityTitle = "Low" + } else if quality <= 480 { + qualityTitle = "Medium" + } else if quality <= 720 { + qualityTitle = "High" + } else { + qualityTitle = "Ultra-High" } - - c?.pushItems(items: .single(ContextController.Items(content: .list(strongSelf.contextMenuVideoQualityItems(dismiss: dismiss))))) - }))) + items.append(.action(ContextMenuActionItem(text: qualityTitle, textLayout: .secondLineWithValue("\(quality)p"), icon: { _ in + if isSelected { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + + guard let self, let videoNode = self.videoNode else { + return + } + videoNode.setVideoQuality(.quality(quality)) + //TODO:release + /*if quality >= 700 { + self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQHD"), color: .white))) + } else { + self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQSD"), color: .white))) + }*/ + + /*if let controller = strongSelf.galleryController() as? GalleryController { + controller.updateSharedPlaybackRate(rate) + }*/ + }))) + } } } else { - if let (message, _, _) = strongSelf.contentInfo() { - let context = strongSelf.context - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in - guard let strongSelf = self, let peer = peer else { - return - } - if let navigationController = strongSelf.baseNavigationController() { - strongSelf.beginCustomDismiss(true) - - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) - - Queue.mainQueue().after(0.3) { - strongSelf.completeCustomDismiss(false) - } - } - f(.default) - }))) - } - - // if #available(iOS 11.0, *) { - // items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in - // f(.default) - // guard let strongSelf = self else { - // return - // } - // strongSelf.beginAirPlaySetup() - // }))) - // } - - if let (message, _, _) = strongSelf.contentInfo() { - for media in message.media { - if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - let url = content.url - - let item = OpenInItem.url(url: url) - let openText = strongSelf.presentationData.strings.Conversation_FileOpenIn - items.append(.action(ContextMenuActionItem(text: openText, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in - f(.default) - - if let strongSelf = self, let controller = strongSelf.galleryController() { - var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - if !presentationData.theme.overallDarkAppearance { - presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) - } - let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in - if let strongSelf = self { - strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {}) - } - }) - controller.present(actionSheet, in: .window(.root)) - } - }))) - break - } - } - } - if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { c, _ in + items.append(.action(ContextMenuActionItem(text: "Save to Gallery", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { c, _ in guard let self else { c?.dismiss(result: .default, completion: nil) return @@ -3281,7 +3415,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { continue } let fileSizeString = dataSizeString(qualityFileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData)) - items.append(.action(ContextMenuActionItem(text: "\(quality)p (\(fileSizeString))", icon: { _ in + items.append(.action(ContextMenuActionItem(text: "Save in \(quality)p", textLayout: .secondLineWithValue(fileSizeString), icon: { _ in return nil }, action: { [weak self] c, _ in c?.dismiss(result: .default, completion: nil) @@ -3351,6 +3485,66 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }))) } + if !items.isEmpty { + items.append(.separator) + } + if let (message, _, _) = strongSelf.contentInfo() { + let context = strongSelf.context + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in + guard let strongSelf = self, let peer = peer else { + return + } + if let navigationController = strongSelf.baseNavigationController() { + strongSelf.beginCustomDismiss(true) + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) + + Queue.mainQueue().after(0.3) { + strongSelf.completeCustomDismiss(false) + } + } + f(.default) + }))) + } + + // if #available(iOS 11.0, *) { + // items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + // f(.default) + // guard let strongSelf = self else { + // return + // } + // strongSelf.beginAirPlaySetup() + // }))) + // } + + if let (message, _, _) = strongSelf.contentInfo() { + for media in message.media { + if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + let url = content.url + + let item = OpenInItem.url(url: url) + let openText = strongSelf.presentationData.strings.Conversation_FileOpenIn + items.append(.action(ContextMenuActionItem(text: openText, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + if let strongSelf = self, let controller = strongSelf.galleryController() { + var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + if !presentationData.theme.overallDarkAppearance { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in + if let strongSelf = self { + strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {}) + } + }) + controller.present(actionSheet, in: .window(.root)) + } + }))) + break + } + } + } + if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in if let self, let navigationController = self.baseNavigationController() { @@ -3377,147 +3571,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - return items + return (items, topItems) } } - - private func contextMenuSpeedItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> { - guard let videoNode = self.videoNode else { - return .single([]) - } - - return videoNode.status - |> take(1) - |> deliverOnMainQueue - |> map { [weak self] status -> [ContextMenuItem] in - guard let status = status, let strongSelf = self else { - return [] - } - - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { c, _ in - c?.popItems() - }))) - - let sliderValuePromise = ValuePromise(nil) - items.append(.custom(SliderContextItem(minValue: 0.2, maxValue: 2.5, value: status.baseRate, valueChanged: { [weak self] newValue, _ in - guard let strongSelf = self, let videoNode = strongSelf.videoNode else { - return - } - let newValue = normalizeValue(newValue) - videoNode.setBaseRate(newValue) - if let controller = strongSelf.galleryController() as? GalleryController { - controller.updateSharedPlaybackRate(newValue) - } - sliderValuePromise.set(newValue) - }), true)) - - items.append(.separator) - - for (text, _, rate) in strongSelf.speedList(strings: strongSelf.presentationData.strings) { - let isSelected = abs(status.baseRate - rate) < 0.01 - items.append(.action(ContextMenuActionItem(text: text, icon: { _ in return nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), signal: sliderValuePromise.get() - |> map { value in - if isSelected && value == nil { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) - } else { - return nil - } - }), action: { _, f in - f(.default) - - guard let strongSelf = self, let videoNode = strongSelf.videoNode else { - return - } - - videoNode.setBaseRate(rate) - if let controller = strongSelf.galleryController() as? GalleryController { - controller.updateSharedPlaybackRate(rate) - } - }))) - } - - return items - } - } - - private func contextMenuVideoQualityItems(dismiss: @escaping () -> Void) -> [ContextMenuItem] { - guard let videoNode = self.videoNode else { - return [] - } - guard let qualityState = videoNode.videoQualityState(), !qualityState.available.isEmpty else { - return [] - } - - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { c, _ in - c?.popItems() - }))) - - do { - let isSelected = qualityState.preferred == .auto - let qualityText: String - if qualityState.current != 0 { - qualityText = "Auto (\(qualityState.current)p)" - } else { - qualityText = "Auto" - } - items.append(.action(ContextMenuActionItem(text: qualityText, icon: { _ in - if isSelected { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) - } else { - return nil - } - }, action: { [weak self] _, f in - f(.default) - - guard let self, let videoNode = self.videoNode else { - return - } - videoNode.setVideoQuality(.auto) - self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white))) - - /*if let controller = strongSelf.galleryController() as? GalleryController { - controller.updateSharedPlaybackRate(rate) - }*/ - }))) - } - - for quality in qualityState.available { - let isSelected = qualityState.preferred == .quality(quality) - items.append(.action(ContextMenuActionItem(text: "\(quality)p", icon: { _ in - if isSelected { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) - } else { - return nil - } - }, action: { [weak self] _, f in - f(.default) - - guard let self, let videoNode = self.videoNode else { - return - } - videoNode.setVideoQuality(.quality(quality)) - if quality >= 700 { - self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQHD"), color: .white))) - } else { - self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQSD"), color: .white))) - } - - /*if let controller = strongSelf.galleryController() as? GalleryController { - controller.updateSharedPlaybackRate(rate) - }*/ - }))) - } - - return items - } private var isAirPlayActive = false private var externalVideoPlayer: ExternalVideoPlayer? diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 3725710918..88243dae3e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -709,6 +709,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private var forceStopAnimations: Bool = false + typealias Params = (item: ChatMessageItem, params: ListViewItemLayoutParams, mergedTop: ChatMessageMerge, mergedBottom: ChatMessageMerge, dateHeaderAtBottom: Bool) + private var currentInputParams: Params? + private var currentApplyParams: ListViewItemApply? + required public init(rotated: Bool) { self.mainContextSourceNode = ContextExtractedContentContainingNode() self.mainContainerNode = ContextControllerSourceNode() @@ -1363,6 +1367,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } + private func internalUpdateLayout() { + if let inputParams = self.currentInputParams, let currentApplyParams = self.currentApplyParams { + let (_, applyLayout) = self.asyncLayout()(inputParams.item, inputParams.params, inputParams.mergedTop, inputParams.mergedBottom, inputParams.dateHeaderAtBottom) + applyLayout(.None, ListViewItemApply(isOnScreen: currentApplyParams.isOnScreen, timestamp: nil), false) + } + } + override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = [] for contentNode in self.contentNodes { @@ -1421,7 +1432,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } private static func beginLayout( - selfReference: Weak, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool, + selfReference: Weak, + _ item: ChatMessageItem, + _ params: ListViewItemLayoutParams, + _ mergedTop: ChatMessageMerge, + _ mergedBottom: ChatMessageMerge, + _ dateHeaderAtBottom: Bool, currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))], authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), viaMeasureLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), @@ -3095,6 +3111,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return (layout, { animation, applyInfo, synchronousLoads in return ChatMessageBubbleItemNode.applyLayout(selfReference: selfReference, animation, synchronousLoads, + inputParams: (item, params, mergedTop, mergedBottom, dateHeaderAtBottom), params: params, applyInfo: applyInfo, layout: layout, @@ -3153,6 +3170,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private static func applyLayout(selfReference: Weak, _ animation: ListViewItemUpdateAnimation, _ synchronousLoads: Bool, + inputParams: Params, params: ListViewItemLayoutParams, applyInfo: ListViewItemApply, layout: ListViewItemNodeLayout, @@ -3209,6 +3227,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return } + strongSelf.currentInputParams = inputParams + strongSelf.currentApplyParams = applyInfo + if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal || item.message.id.namespace == Namespaces.Message.QuickReplyLocal { strongSelf.wasPending = true } @@ -4025,10 +4046,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI contextSourceNode?.updateDistractionFreeMode?(value) } contentNode.requestInlineUpdate = { [weak strongSelf] in - guard let strongSelf, let item = strongSelf.item else { + guard let strongSelf else { return } - item.controllerInteraction.requestMessageUpdate(item.message.id, false) + + strongSelf.internalUpdateLayout() } contentNode.updateIsExtractedToContextPreview(contextSourceNode.isExtractedToContextPreview) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 1d9e08a4b9..c15247415a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -627,7 +627,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } public func isAvailableForGalleryTransition() -> Bool { - return self.automaticPlayback ?? false + if let automaticPlayback = self.automaticPlayback, automaticPlayback, self.decoration != nil { + return true + } else { + return false + } } public func isAvailableForInstantPageTransition() -> Bool { @@ -1094,9 +1098,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } else { mediaUpdated = true } - if hlsInlinePlaybackRange != appliedHlsInlinePlaybackRange { - mediaUpdated = true - } + let inlinePlaybackRangeUpdated = hlsInlinePlaybackRange != appliedHlsInlinePlaybackRange var isSendingUpdated = false if let currentMessage = currentMessage { @@ -1154,7 +1156,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } - if mediaUpdated || isSendingUpdated || automaticPlaybackUpdated { + let reloadMedia = mediaUpdated || isSendingUpdated || automaticPlaybackUpdated + if mediaUpdated || isSendingUpdated || automaticPlaybackUpdated || inlinePlaybackRangeUpdated { var media = media var extendedMedia: TelegramExtendedMedia? @@ -1381,31 +1384,6 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr chatMessageWebFileCancelInteractiveFetch(account: context.account, image: image) }) } else if var file = media as? TelegramMediaFile { - if isSecretMedia { - updateImageSignal = { synchronousLoad, _ in - return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file)) - } - } else { - if file.isAnimatedSticker { - let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) - updateImageSignal = { synchronousLoad, _ in - return chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))) - } - } else if file.isSticker || file.isVideoSticker { - updateImageSignal = { synchronousLoad, _ in - return chatMessageSticker(account: context.account, userLocation: .peer(message.id.peerId), file: file, small: false) - } - } else { - onlyFullSizeVideoThumbnail = isSendingUpdated - updateImageSignal = { synchronousLoad, _ in - return mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) - } - updateBlurredImageSignal = { synchronousLoad, _ in - return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: true) - } - } - } - var uploading = false if file.resource is VideoLibraryMediaResource { uploading = true @@ -1463,6 +1441,31 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } + if isSecretMedia { + updateImageSignal = { synchronousLoad, _ in + return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file)) + } + } else { + if file.isAnimatedSticker { + let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) + updateImageSignal = { synchronousLoad, _ in + return chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))) + } + } else if file.isSticker || file.isVideoSticker { + updateImageSignal = { synchronousLoad, _ in + return chatMessageSticker(account: context.account, userLocation: .peer(message.id.peerId), file: file, small: false) + } + } else { + onlyFullSizeVideoThumbnail = isSendingUpdated + updateImageSignal = { synchronousLoad, _ in + return mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) + } + updateBlurredImageSignal = { synchronousLoad, _ in + return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: true) + } + } + } + updatedFetchControls = FetchControls(fetch: { manual in if let strongSelf = self { if file.isAnimated { @@ -1531,6 +1534,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } } + if !reloadMedia { + updateImageSignal = nil + } var isExtendedMedia = false if statusUpdated { @@ -1988,19 +1994,18 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr guard let strongSelf else { return } + let hlsInlinePlaybackRange: Range? if let preloadData { - strongSelf.hlsInlinePlaybackRange = preloadData.1 + hlsInlinePlaybackRange = preloadData.1 } else { - strongSelf.hlsInlinePlaybackRange = nil + hlsInlinePlaybackRange = nil + } + if strongSelf.hlsInlinePlaybackRange != hlsInlinePlaybackRange { + strongSelf.hlsInlinePlaybackRange = hlsInlinePlaybackRange + strongSelf.requestInlineUpdate?() } - strongSelf.requestInlineUpdate?() }) } - } else { - if let hlsInlinePlaybackRangeDisposable = strongSelf.hlsInlinePlaybackRangeDisposable { - strongSelf.hlsInlinePlaybackRangeDisposable = nil - hlsInlinePlaybackRangeDisposable.dispose() - } } } }) diff --git a/submodules/TelegramUI/Components/SectionTitleContextItem/BUILD b/submodules/TelegramUI/Components/SectionTitleContextItem/BUILD new file mode 100644 index 0000000000..aa4cbd29ef --- /dev/null +++ b/submodules/TelegramUI/Components/SectionTitleContextItem/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SectionTitleContextItem", + module_name = "SectionTitleContextItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ContextUI", + "//submodules/TelegramPresentationData", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SectionTitleContextItem/Sources/SectionTitleContextItem.swift b/submodules/TelegramUI/Components/SectionTitleContextItem/Sources/SectionTitleContextItem.swift new file mode 100644 index 0000000000..03cb0e9a23 --- /dev/null +++ b/submodules/TelegramUI/Components/SectionTitleContextItem/Sources/SectionTitleContextItem.swift @@ -0,0 +1,89 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ContextUI +import TelegramPresentationData + +public final class SectionTitleContextItem: ContextMenuCustomItem { + let text: String + + public init(text: String) { + self.text = text + } + + public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + return SectionTitleContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) + } +} + +private final class SectionTitleContextItemNode: ASDisplayNode, ContextMenuCustomNode { + private let item: SectionTitleContextItem + private let presentationData: PresentationData + private let getController: () -> ContextControllerProtocol? + private let actionSelected: (ContextMenuActionResult) -> Void + + private let backgroundNode: ASDisplayNode + private let textNode: ImmediateTextNode + + var needsSeparator: Bool { + return false + } + + init(presentationData: PresentationData, item: SectionTitleContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + self.item = item + self.presentationData = presentationData + self.getController = getController + self.actionSelected = actionSelected + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 12.0 / 17.0) + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isAccessibilityElement = false + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor + + self.textNode = ImmediateTextNode() + self.textNode.isAccessibilityElement = false + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + self.textNode.attributedText = NSAttributedString(string: item.text, font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor) + self.textNode.maximumNumberOfLines = 1 + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textNode) + } + + func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { + let sideInset: CGFloat = 16.0 + + let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - sideInset, height: .greatestFiniteMagnitude)) + let height: CGFloat = 28.0 + + return (CGSize(width: textSize.width + sideInset + sideInset, height: height), { size, transition in + let verticalOrigin = floor((size.height - textSize.height) / 2.0) + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize) + transition.updateFrameAdditive(node: self.textNode, frame: textFrame) + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + }) + } + + func updateTheme(presentationData: PresentationData) { + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 12.0 / 17.0) + self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor) + } + + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } +} diff --git a/submodules/TelegramUI/Components/SliderContextItem/Sources/SliderContextItem.swift b/submodules/TelegramUI/Components/SliderContextItem/Sources/SliderContextItem.swift index a96132f715..bd7a4fef68 100644 --- a/submodules/TelegramUI/Components/SliderContextItem/Sources/SliderContextItem.swift +++ b/submodules/TelegramUI/Components/SliderContextItem/Sources/SliderContextItem.swift @@ -8,12 +8,14 @@ import TelegramPresentationData import AnimatedCountLabelNode public final class SliderContextItem: ContextMenuCustomItem { + private let title: String? private let minValue: CGFloat private let maxValue: CGFloat private let value: CGFloat private let valueChanged: (CGFloat, Bool) -> Void - public init(minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + public init(title: String? = nil, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + self.title = title self.minValue = minValue self.maxValue = maxValue self.value = value @@ -21,7 +23,7 @@ public final class SliderContextItem: ContextMenuCustomItem { } public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { - return SliderContextItemNode(presentationData: presentationData, getController: getController, minValue: self.minValue, maxValue: self.maxValue, value: self.value, valueChanged: self.valueChanged) + return SliderContextItemNode(presentationData: presentationData, getController: getController, title: self.title, minValue: self.minValue, maxValue: self.maxValue, value: self.value, valueChanged: self.valueChanged) } } @@ -31,12 +33,18 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, private var presentationData: PresentationData private(set) var vibrancyEffectView: UIVisualEffectView? + + private let backgroundTitleNode: ImmediateTextNode + private let dimBackgroundTitleNode: ImmediateTextNode + private let foregroundTitleNode: ImmediateTextNode + private let backgroundTextNode: ImmediateAnimatedCountLabelNode private let dimBackgroundTextNode: ImmediateAnimatedCountLabelNode private let foregroundNode: ASDisplayNode private let foregroundTextNode: ImmediateAnimatedCountLabelNode + let title: String? let minValue: CGFloat let maxValue: CGFloat var value: CGFloat = 1.0 { @@ -49,13 +57,18 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, private let hapticFeedback = HapticFeedback() - init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, title: String?, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { self.presentationData = presentationData + self.title = title self.minValue = minValue self.maxValue = maxValue self.value = value self.valueChanged = valueChanged + self.backgroundTitleNode = ImmediateTextNode() + self.dimBackgroundTitleNode = ImmediateTextNode() + self.foregroundTitleNode = ImmediateTextNode() + self.backgroundTextNode = ImmediateAnimatedCountLabelNode() self.backgroundTextNode.alwaysOneDirection = true @@ -76,7 +89,6 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, self.isUserInteractionEnabled = true if presentationData.theme.overallDarkAppearance { - } else { let style: UIBlurEffect.Style style = .extraLight @@ -87,9 +99,12 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, self.vibrancyEffectView = vibrancyEffectView } + self.addSubnode(self.backgroundTitleNode) + self.addSubnode(self.dimBackgroundTitleNode) self.addSubnode(self.backgroundTextNode) self.addSubnode(self.dimBackgroundTextNode) self.addSubnode(self.foregroundNode) + self.foregroundNode.addSubnode(self.foregroundTitleNode) self.foregroundNode.addSubnode(self.foregroundTextNode) let stringValue = "1.0x" @@ -114,6 +129,11 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, textCount += 1 } } + + self.backgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: backgroundTextColor) + self.dimBackgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: dimBackgroundTextColor) + self.foregroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: foregroundTextColor) + self.dimBackgroundTextNode.segments = dimBackgroundSegments self.backgroundTextNode.segments = backgroundSegments self.foregroundTextNode.segments = foregroundSegments @@ -179,6 +199,10 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, self.backgroundTextNode.segments = backgroundSegments self.foregroundTextNode.segments = foregroundSegments + self.backgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: backgroundTextColor) + self.dimBackgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: dimBackgroundTextColor) + self.foregroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: foregroundTextColor) + let _ = self.dimBackgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: transition.isAnimated) let _ = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: transition.isAnimated) let _ = self.foregroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: transition.isAnimated) @@ -188,20 +212,41 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, let valueWidth: CGFloat = 70.0 let height: CGFloat = 45.0 - var backgroundTextSize = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: true) + let originalBackgroundTextSize = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: true) + var backgroundTextSize = originalBackgroundTextSize backgroundTextSize.width = valueWidth + let backgroundTitleSize = self.backgroundTitleNode.updateLayout(CGSize(width: 120.0, height: 100.0)) + let _ = self.dimBackgroundTitleNode.updateLayout(CGSize(width: 120.0, height: 100.0)) + let _ = self.foregroundTitleNode.updateLayout(CGSize(width: 120.0, height: 100.0)) + return (CGSize(width: height * 3.0, height: height), { size, transition in let leftInset: CGFloat = 17.0 self.vibrancyEffectView?.frame = CGRect(origin: .zero, size: size) - let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - backgroundTextSize.height) / 2.0)), size: backgroundTextSize) + let backgroundTextWidth = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: true).width + + self.updateValue(transition: transition) + + let titleFrame: CGRect + let textFrame: CGRect + + titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - backgroundTitleSize.height) / 2.0)), size: backgroundTitleSize) + + if self.title != nil { + textFrame = CGRect(origin: CGPoint(x: size.width - leftInset - backgroundTextWidth, y: floor((height - backgroundTextSize.height) / 2.0)), size: backgroundTextSize) + } else { + textFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - backgroundTextSize.height) / 2.0)), size: backgroundTextSize) + } + + transition.updateFrameAdditive(node: self.dimBackgroundTitleNode, frame: titleFrame) + transition.updateFrameAdditive(node: self.backgroundTitleNode, frame: titleFrame) + transition.updateFrameAdditive(node: self.foregroundTitleNode, frame: titleFrame) + transition.updateFrameAdditive(node: self.dimBackgroundTextNode, frame: textFrame) transition.updateFrameAdditive(node: self.backgroundTextNode, frame: textFrame) transition.updateFrameAdditive(node: self.foregroundTextNode, frame: textFrame) - - self.updateValue(transition: transition) }) } diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/Contents.json new file mode 100644 index 0000000000..b00344b3db --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "settings_24 (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/settings_24 (1).pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/settings_24 (1).pdf new file mode 100644 index 0000000000..ec496f75d6 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/settings_24 (1).pdf differ diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift index 32e0189495..27942f6050 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift @@ -26,7 +26,7 @@ public final class HLSQualitySet { for attribute in alternativeFile.attributes { if case let .Video(_, size, _, _, _, videoCodec) = attribute { if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) { - let key = Int(size.height) + let key = Int(min(size.width, size.height)) if let currentFile = qualityFiles[key] { var currentCodec: String? for attribute in currentFile.media.attributes { diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift index 8f7d4e1c0a..b189b313ff 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift @@ -1127,7 +1127,9 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions) - self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: fileReference, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true) |> map { [weak self] getSize, getData in + let thumbnailVideoReference = HLSVideoContent.minimizedHLSQuality(file: fileReference)?.file ?? fileReference + + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: thumbnailVideoReference, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true) |> map { [weak self] getSize, getData in Queue.mainQueue().async { if let strongSelf = self, strongSelf.dimensions == nil { if let dimensions = getSize() { @@ -1557,7 +1559,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod case .auto: self.requestedLevelIndex = nil case let .quality(quality): - if let level = self.playerAvailableLevels.first(where: { $0.value.height == quality }) { + if let level = self.playerAvailableLevels.first(where: { min($0.value.width, $0.value.height) == quality }) { self.requestedLevelIndex = level.key } else { self.requestedLevelIndex = nil @@ -1577,10 +1579,10 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod return nil } - var available = self.playerAvailableLevels.values.map(\.height) + var available = self.playerAvailableLevels.values.map { min($0.width, $0.height) } available.sort(by: { $0 > $1 }) - return (currentLevel.height, self.preferredVideoQuality, available) + return (min(currentLevel.width, currentLevel.height), self.preferredVideoQuality, available) } func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int {