From 21adc7d0618f29467ce5ed5fadbb8f72cada730d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 30 Mar 2026 02:55:35 +0200 Subject: [PATCH] Various improvements --- .../Sources/ListMessageFileItemNode.swift | 7 +- .../MediaNavigationAccessoryHeaderNode.swift | 5 +- .../Network/FetchedMediaResource.swift | 5 + .../Messages/PendingStoryManager.swift | 7 +- .../Sources/MediaEditorValues.swift | 6 +- .../Sources/MediaEditorScreen.swift | 14 +- .../Sources/MediaEditorStoryCompletion.swift | 29 ++-- .../MediaNavigationAccessoryHeaderNode.swift | 5 +- .../Sources/PlainButtonComponent.swift | 6 + .../Sources/ForwardInfoPanelComponent.swift | 106 +++++++++++++ .../StoryContentCaptionComponent.swift | 87 +++++++++-- .../StoryItemSetContainerComponent.swift | 146 +++++++++++++++++- .../OverlayAudioPlayerControllerNode.swift | 2 +- .../Sources/PollResultsController.swift | 4 + 14 files changed, 388 insertions(+), 41 deletions(-) diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 4505aaf2dc..75d4055d15 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -524,7 +524,7 @@ public final class ListMessageFileItemNode: ListMessageNode { } if isExtracted { - strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.theme.list.plainBackgroundColor) + strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.theme.list.itemModalBlocksBackgroundColor) } if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { @@ -1036,7 +1036,10 @@ public final class ListMessageFileItemNode: ListMessageNode { reorderInset = sizeAndApply.0 } - let contentRightInset = rightInset + rightOffset + reorderInset + var contentRightInset = rightInset + rightOffset + reorderInset + if !item.isGlobalSearchResult { + contentRightInset += 16.0 + } let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message?.timestamp ?? 0, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat) diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index 4cca7d51fe..e76f43d4a4 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -279,12 +279,10 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.scrollNode.addSubnode(self.previousItemNode) self.scrollNode.addSubnode(self.nextItemNode) - self.addSubnode(self.closeButton) self.addSubnode(self.rateButton) self.addSubnode(self.accessibilityAreaNode) self.actionButton.addSubnode(self.playPauseIconNode) - self.addSubnode(self.actionButton) self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside) self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) @@ -296,6 +294,9 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.addSubnode(self.scrubbingNode) + self.addSubnode(self.actionButton) + self.addSubnode(self.closeButton) + self.addSubnode(self.separatorNode) self.actionButton.highligthedChanged = { [weak self] highlighted in diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index 40fd07692b..553b840e4f 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -175,6 +175,11 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me return representation.resource } } + if let video = image.video { + if let resource = findMediaResource(media: video, previousMedia: previousMedia, resource: resource) { + return resource + } + } } else if let file = media as? TelegramMediaFile { if areResourcesEqual(file.resource, resource) { return file.resource diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift index 0859aeb5c7..fd0f176dc1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift @@ -201,8 +201,11 @@ public extension Stories { self.folders = try container.decodeIfPresent([Int64].self, forKey: .folders) ?? [] - let musicData = try container.decode(Data.self, forKey: .music) - self.music = PostboxDecoder(buffer: MemoryBuffer(data: musicData)).decodeRootObject() as? TelegramMediaFile + if let musicData = try container.decodeIfPresent(Data.self, forKey: .music) { + self.music = PostboxDecoder(buffer: MemoryBuffer(data: musicData)).decodeRootObject() as? TelegramMediaFile + } else { + self.music = nil + } self.uploadInfo = try container.decodeIfPresent(StoryUploadInfo.self, forKey: .uploadInfo) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index 6fbf34e316..dc285429ae 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -66,23 +66,27 @@ public struct MediaAudioTrack: Codable, Equatable { case artist case title case duration + case file } public let path: String public let artist: String? public let title: String? public let duration: Double + public let file: TelegramMediaFile? public init( path: String, artist: String?, title: String?, - duration: Double + duration: Double, + file: TelegramMediaFile? = nil ) { self.path = path self.artist = artist self.title = title self.duration = duration + self.file = file } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 1c565ad880..f9f3e01ef5 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -5163,7 +5163,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return } - self.insertAudio(path: copyPath, fileName: fileName) + self.insertAudio(path: copyPath, fileName: fileName, file: file.media as? TelegramMediaFile) } }) } @@ -5224,7 +5224,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID }), in: .window(.root)) } - private func insertAudio(path: String, fileName: String, dispose: (() -> Void)? = nil) { + private func insertAudio(path: String, fileName: String, file: TelegramMediaFile? = nil, dispose: (() -> Void)? = nil) { guard let mediaEditor = self.mediaEditor else { return } @@ -5300,7 +5300,15 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID audioTrimRange = 0 ..< min(15, audioDuration) } - mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration), trimRange: audioTrimRange, offset: audioOffset) + var passFile = false + if let file { + for attribute in file.attributes { + if case let .Audio(_, _, title, _, _) = attribute, let title, !title.isEmpty { + passFile = true + } + } + } + mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration, file: passFile ? file : nil), trimRange: audioTrimRange, offset: audioOffset) mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: true) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift index 77b74fb29c..0f51bd6e6a 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift @@ -129,7 +129,7 @@ extension MediaEditorScreenImpl { if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) { self.saveDraft(id: randomId, isEdit: true) - self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, music: nil, randomId: randomId)], { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, music: mediaEditor.values.audioTrack?.file, randomId: randomId)], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -464,7 +464,7 @@ extension MediaEditorScreenImpl { return } Logger.shared.log("MediaEditor", "Completed with video \(videoResult)") - self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: values, duration: duration, dimensions: values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, music: nil, randomId: randomId)], { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: values, duration: duration, dimensions: values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, music: values.audioTrack?.file, randomId: randomId)], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -506,7 +506,7 @@ extension MediaEditorScreenImpl { return } Logger.shared.log("MediaEditor", "Completed with image \(resultImage)") - self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, music: nil, randomId: randomId)], { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, music: values.audioTrack?.file, randomId: randomId)], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -636,7 +636,7 @@ extension MediaEditorScreenImpl { } guard let avAsset else { Queue.mainQueue().async { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) } return } @@ -683,19 +683,19 @@ extension MediaEditorScreenImpl { coverTimestamp: itemMediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, - music: nil, + music: itemMediaEditor.values.audioTrack?.file, randomId: randomId ) completion(result) } else { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) } } } else { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) } } else { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) } } } @@ -737,7 +737,7 @@ extension MediaEditorScreenImpl { return } guard let image else { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) return } itemMediaEditor.replaceSource(image, additionalImage: nil, time: .zero, mirror: false) @@ -766,16 +766,16 @@ extension MediaEditorScreenImpl { coverTimestamp: nil, options: self.state.privacy, stickers: stickers, - music: nil, + music: itemMediaEditor.values.audioTrack?.file, randomId: randomId ) completion(result) } else { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) } } } else { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) } } @@ -818,7 +818,6 @@ extension MediaEditorScreenImpl { mode: .default, subject: editorSubject, values: values, - hasHistogram: false, isStandalone: true ) } @@ -842,7 +841,7 @@ extension MediaEditorScreenImpl { } } - private func createEmptyResult(randomId: Int64) -> MediaEditorScreenImpl.Result { + private func createEmptyResult(randomId: Int64, music: TelegramMediaFile? = nil) -> MediaEditorScreenImpl.Result { let emptyImage = UIImage() return MediaEditorScreenImpl.Result( media: .image( @@ -854,7 +853,7 @@ extension MediaEditorScreenImpl { coverTimestamp: nil, options: self.state.privacy, stickers: [], - music: nil, + music: music, randomId: randomId ) } diff --git a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift index 2760ca964b..21b4d08ef8 100644 --- a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -280,12 +280,10 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.scrollNode.addSubnode(self.previousItemNode) self.scrollNode.addSubnode(self.nextItemNode) - self.addSubnode(self.closeButton) self.addSubnode(self.rateButton) self.addSubnode(self.accessibilityAreaNode) self.actionButton.addSubnode(self.playPauseIconNode) - self.addSubnode(self.actionButton) self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside) self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) @@ -297,6 +295,9 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.addSubnode(self.scrubbingNode) + self.addSubnode(self.actionButton) + self.addSubnode(self.closeButton) + self.actionButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift index fadcc5c0d6..02cf87e2bb 100644 --- a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -20,6 +20,7 @@ public final class PlainButtonComponent: Component { public let animateAlpha: Bool public let animateScale: Bool public let animateContents: Bool + public let rasterizeOnScaleAnimation: Bool public let tag: AnyObject? public init( @@ -33,6 +34,7 @@ public final class PlainButtonComponent: Component { animateAlpha: Bool = true, animateScale: Bool = true, animateContents: Bool = true, + rasterizeOnScaleAnimation: Bool = true, tag: AnyObject? = nil ) { self.content = content @@ -45,6 +47,7 @@ public final class PlainButtonComponent: Component { self.animateAlpha = animateAlpha self.animateScale = animateScale self.animateContents = animateContents + self.rasterizeOnScaleAnimation = rasterizeOnScaleAnimation self.tag = tag } @@ -76,6 +79,9 @@ public final class PlainButtonComponent: Component { if lhs.animateContents != rhs.animateContents { return false } + if lhs.rasterizeOnScaleAnimation != rhs.rasterizeOnScaleAnimation { + return false + } if lhs.tag !== rhs.tag { return false } diff --git a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift index 6cf95f03eb..d8c58d4059 100644 --- a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift @@ -206,3 +206,109 @@ public final class ForwardInfoPanelComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +public final class MusicPanelComponent: Component { + public let context: AccountContext + public let file: TelegramMediaFile + + public init( + context: AccountContext, + file: TelegramMediaFile + ) { + self.context = context + self.file = file + } + + public static func ==(lhs: MusicPanelComponent, rhs: MusicPanelComponent) -> Bool { + if lhs.file != rhs.file { + return false + } + return true + } + + public final class View: UIView { + public let backgroundView: UIImageView + private let blurBackgroundView: UIVisualEffectView + private var iconView = UIImageView() + private var title = ComponentView() + private var text = ComponentView() + + private var component: MusicPanelComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + if #available(iOS 13.0, *) { + self.blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialDark)) + } else { + self.blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + } + self.blurBackgroundView.clipsToBounds = true + + self.backgroundView = UIImageView() + self.backgroundView.image = generateStretchableFilledCircleImage(radius: 4.0, color: UIColor(white: 0.0, alpha: 0.4)) + + super.init(frame: frame) + + self.iconView.image = UIImage(bundleImageName: "Media Editor/SmallAudio") + self.addSubview(self.iconView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: MusicPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + var titleOffset: CGFloat = 0.0 + if let image = self.iconView.image { + self.iconView.frame = CGRect(origin: CGPoint(x: 10.0, y: 5.0), size: image.size) + } + titleOffset += 29.0 + + let text = NSMutableAttributedString() + for attribute in component.file.attributes { + if case let .Audio(_, _, title, performer, _) = attribute, let title, !title.isEmpty { + text.append(NSAttributedString(string: title, font: Font.semibold(13.0), textColor: .white)) + if let performer, !performer.isEmpty { + text.append(NSAttributedString(string: " • ", font: Font.semibold(13.0), textColor: .white.withAlphaComponent(0.28))) + text.append(NSAttributedString(string: performer, font: Font.regular(13.0), textColor: .white)) + } + } + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(text), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - titleOffset - 20.0, height: availableSize.height) + ) + let titleFrame = CGRect(origin: CGPoint(x: titleOffset, y: 5.0 - UIScreenPixel), size: titleSize) + if let view = self.title.view { + if view.superview == nil { + self.addSubview(view) + } + view.frame = titleFrame + } + + let size = CGSize(width: titleSize.width + 39.0, height: 26.0) + self.blurBackgroundView.frame = CGRect(origin: .zero, size: size) + self.insertSubview(self.blurBackgroundView, at: 0) + self.blurBackgroundView.layer.cornerRadius = size.height / 2.0 + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index c267b8633c..49d6030c89 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -70,6 +70,7 @@ final class StoryContentCaptionComponent: Component { let textSelectionAction: (NSAttributedString, TextSelectionAction) -> Void let controller: () -> ViewController? let openStory: (EnginePeer, EngineStoryItem?) -> Void + let openMusic: (FileMediaReference, UIView) -> Void init( externalState: ExternalState, @@ -87,7 +88,8 @@ final class StoryContentCaptionComponent: Component { longTapAction: @escaping (Action) -> Void, textSelectionAction: @escaping (NSAttributedString, TextSelectionAction) -> Void, controller: @escaping () -> ViewController?, - openStory: @escaping (EnginePeer, EngineStoryItem?) -> Void + openStory: @escaping (EnginePeer, EngineStoryItem?) -> Void, + openMusic: @escaping (FileMediaReference, UIView) -> Void ) { self.externalState = externalState self.context = context @@ -105,6 +107,7 @@ final class StoryContentCaptionComponent: Component { self.textSelectionAction = textSelectionAction self.controller = controller self.openStory = openStory + self.openMusic = openMusic } static func ==(lhs: StoryContentCaptionComponent, rhs: StoryContentCaptionComponent) -> Bool { @@ -187,6 +190,8 @@ final class StoryContentCaptionComponent: Component { private let scrollBottomFullMaskView: UIView private let scrollTopMaskView: UIImageView + private var musicPanel: ComponentView? + private var forwardInfoPanel: ComponentView? private var forwardInfoDisposable: Disposable? private var forwardInfoStory: EngineStoryItem? @@ -316,10 +321,10 @@ final class StoryContentCaptionComponent: Component { let contentItem = self.isExpanded ? self.expandedText : self.collapsedText - if let textView = contentItem.textNode?.textNode.view { - let textLocalPoint = self.convert(point, to: textView) - if textLocalPoint.y >= -7.0 { - return self.textSelectionNode?.view ?? textView + if let musicView = self.musicPanel?.view { + let musicLocalPoint = self.convert(point, to: musicView) + if let result = musicView.hitTest(musicLocalPoint, with: nil) { + return result } } @@ -330,6 +335,13 @@ final class StoryContentCaptionComponent: Component { } } + if let textView = contentItem.textNode?.textNode.view { + let textLocalPoint = self.convert(point, to: textView) + if textLocalPoint.y >= -7.0 { + return self.textSelectionNode?.view ?? textView + } + } + return nil } @@ -617,7 +629,8 @@ final class StoryContentCaptionComponent: Component { self.state = state let sideInset: CGFloat = 16.0 - let verticalInset: CGFloat = 7.0 + let verticalInset: CGFloat = 8.0 + let bottomPanelSpacing: CGFloat = 5.0 let textContainerSize = CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - verticalInset * 2.0) var baseQuoteSecondaryTintColor: UIColor? @@ -716,6 +729,57 @@ final class StoryContentCaptionComponent: Component { let visibleTextHeight = collapsedTextLayout.0.size.height - textInsets.top - textInsets.bottom let textOverflowHeight: CGFloat = expandedTextLayout.0.size.height - textInsets.top - textInsets.bottom - visibleTextHeight let scrollContentSize = CGSize(width: availableSize.width, height: availableSize.height + textOverflowHeight) + let hasBottomStackContent = !component.text.isEmpty || component.forwardInfo != nil + var bottomContentOffset: CGFloat = 0.0 + + if let music = component.music?._asMedia() as? TelegramMediaFile { + let musicPanel: ComponentView + if let current = self.musicPanel { + musicPanel = current + } else { + musicPanel = ComponentView() + self.musicPanel = musicPanel + } + + let musicPanelSize = musicPanel.update( + transition: .immediate, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + MusicPanelComponent( + context: component.context, + file: music + ) + ), + action: { [weak self] in + guard let self else { + return + } + if let sourceView = self.musicPanel?.view { + self.component?.openMusic(.standalone(media: music), sourceView) + } + }, + animateScale: false + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + bottomContentOffset = musicPanelSize.height + (hasBottomStackContent ? bottomPanelSpacing : 0.0) + let musicPanelFrame = CGRect(origin: CGPoint(x: sideInset - 8.0, y: availableSize.height - verticalInset - musicPanelSize.height), size: musicPanelSize) + if let view = musicPanel.view { + if view.superview == nil { + self.scrollView.addSubview(view) + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + } + view.frame = musicPanelFrame + } + } else if let musicPanel = self.musicPanel { + self.musicPanel = nil + musicPanel.view?.removeFromSuperview() + } + + let contentBottomY = availableSize.height - verticalInset - bottomContentOffset if let forwardInfo = component.forwardInfo { let authorName: String @@ -778,8 +842,6 @@ final class StoryContentCaptionComponent: Component { fillsWidth: false ) ), - effectAlignment: .center, - minSize: nil, action: { [weak self] in if let self, case let .known(peer, _, _) = forwardInfo { self.component?.openStory(peer, self.forwardInfoStory) @@ -792,13 +854,14 @@ final class StoryContentCaptionComponent: Component { return nil })) } - } + }, + animateScale: false ) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) ) - let forwardInfoPanelFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset - forwardInfoPanelSize.height - 10.0), size: forwardInfoPanelSize) + let forwardInfoPanelFrame = CGRect(origin: CGPoint(x: sideInset, y: contentBottomY - visibleTextHeight - forwardInfoPanelSize.height - bottomPanelSpacing), size: forwardInfoPanelSize) if let view = forwardInfoPanel.view { if view.superview == nil { self.scrollView.addSubview(view) @@ -813,8 +876,8 @@ final class StoryContentCaptionComponent: Component { } - let collapsedTextFrame = CGRect(origin: CGPoint(x: sideInset - textInsets.left, y: availableSize.height - visibleTextHeight - verticalInset - textInsets.top), size: collapsedTextLayout.0.size) - let expandedTextFrame = CGRect(origin: CGPoint(x: sideInset - textInsets.left, y: availableSize.height - visibleTextHeight - verticalInset - textInsets.top), size: expandedTextLayout.0.size) + let collapsedTextFrame = CGRect(origin: CGPoint(x: sideInset - textInsets.left, y: contentBottomY - visibleTextHeight - textInsets.top), size: collapsedTextLayout.0.size) + let expandedTextFrame = CGRect(origin: CGPoint(x: sideInset - textInsets.left, y: contentBottomY - visibleTextHeight - textInsets.top), size: expandedTextLayout.0.size) var spoilerExpandRect: CGRect? if let location = self.displayContentsUnderSpoilers.location { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 6cff603f75..e1b6ffa7c0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -4523,7 +4523,7 @@ public final class StoryItemSetContainerComponent: Component { } } - if !isUnsupported, !component.slice.item.storyItem.text.isEmpty || component.slice.item.storyItem.forwardInfo != nil { + if !isUnsupported, !component.slice.item.storyItem.text.isEmpty || component.slice.item.storyItem.forwardInfo != nil || component.slice.item.storyItem.music != nil { var captionItemTransition = transition let captionItem: CaptionItem if let current = self.captionItem { @@ -4709,6 +4709,12 @@ public final class StoryItemSetContainerComponent: Component { } } } + }, + openMusic: { [weak self] file, sourceView in + guard let self else { + return + } + self.performMusicAction(file: file, sourceView: sourceView, gesture: nil) } )), environment: {}, @@ -7541,6 +7547,144 @@ public final class StoryItemSetContainerComponent: Component { }) } + private func performMusicAction(file: FileMediaReference, sourceView: UIView, gesture: ContextGesture?) { + guard let component = self.component, let controller = component.controller() else { + return + } + + self.dismissAllTooltips() + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + + let items = component.context.engine.peers.savedMusicIds() + |> take(1) + |> map { [weak self] savedIds -> ContextController.Items in + var items: [ContextMenuItem] = [] + + items.append( + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + if let self { + var subActions: [ContextMenuItem] = [] +// subActions.append( +// .action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { c, _ in +// c?.popItems() +// })) +// ) +// subActions.append(.separator) + + subActions.append( + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Profile, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.peers.addSavedMusic(file: file).start() + + guard let controller = component.controller() as? StoryContainerScreen else { + return + } + + let overlayController = UndoOverlayController( + presentationData: presentationData, + content: .universalImage( + image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!, + size: nil, + title: nil, + text: presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile, + customUndoText: presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile_View, + timeout: 3.0 + ), + action: { [weak self] action in + guard let self, let component = self.component, case .undo = action else { + return false + } + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let component = self.component, let peer else { + return + } + guard let controller = component.controller() as? StoryContainerScreen else { + return + } + guard let navigationController = controller.navigationController as? NavigationController else { + return + } + if let controller = component.context.sharedContext.makePeerInfoController( + context: component.context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .myProfile, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + navigationController.pushViewController(controller) + } + }) + return true + } + ) + controller.present(overlayController, in: .current) + })) + ) + + subActions.append( + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_SavedMessages, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.messages.enqueueOutgoingMessage(to: component.context.account.peerId, replyTo: nil, content: .file(file)).start() + + guard let controller = component.controller() as? StoryContainerScreen else { + return + } + + let overlayController = UndoOverlayController( + presentationData: presentationData, + content: .forward(savedMessages: true, text: presentationData.strings.MediaPlayer_AudioForwardedToSavedMesagesTooltip), + action: { _ in + return true + } + ) + controller.present(overlayController, in: .current) + })) + ) + + let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil + subActions.append( + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Info, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: noAction)) + ) + + c?.pushItems(items: .single(ContextController.Items(content: .list(subActions)))) + } + })) + ) + return ContextController.Items(content: .list(items)) + } + + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .top)), + items: items, + gesture: gesture + ) + contextController.dismissed = { [weak self] in + guard let self else { + return + } + self.contextController = nil + self.updateIsProgressPaused() + } + self.contextController = contextController + self.updateIsProgressPaused() + controller.present(contextController, in: .window(.root)) + } + private func beginPictureInPicture() { guard let component = self.component, let visibleItem = self.visibleItems[component.slice.item.id] else { return diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 131c7a4632..1324d32411 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -963,7 +963,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.updateHistoryContentOffset(self.historyNode.visibleContentOffset(), transition: transition) var layout = layout - layout.intrinsicInsets.bottom = controlsHeight + 8.0 + layout.intrinsicInsets.bottom = controlsHeight + (self.historyNode.hasAnyMessages ? 0.0 : 8.0) self.getParentController()?.presentationContext.containerLayoutUpdated(layout, transition: transition) } diff --git a/submodules/TelegramUI/Sources/PollResultsController.swift b/submodules/TelegramUI/Sources/PollResultsController.swift index 7650c459da..fb9234290f 100644 --- a/submodules/TelegramUI/Sources/PollResultsController.swift +++ b/submodules/TelegramUI/Sources/PollResultsController.swift @@ -464,6 +464,10 @@ public func pollResultsController(context: AccountContext, messageId: EngineMess resultsContext.state ) |> map { presentationData, state, resultsState -> (ItemListControllerState, (ItemListNodeState, Any)) in + var presentationData = presentationData + let updatedTheme = presentationData.theme.withModalBlocksBackground() + presentationData = presentationData.withUpdated(theme: updatedTheme) + var isEmpty = false for (_, optionState) in resultsState.options { if !optionState.hasLoadedOnce {