From 1e56d1b8ff89a0c44456f4ad31066bec41dd22ba Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 27 Jun 2023 23:41:17 +0300 Subject: [PATCH 1/2] Stories --- submodules/Display/Source/NavigationBar.swift | 7 +- .../MediaPlayer/Sources/MediaPlayer.swift | 26 +- submodules/Postbox/Sources/Postbox.swift | 6 +- .../Messages/PendingStoryManager.swift | 27 +- .../Sources/MessageInputPanelComponent.swift | 6 +- .../Sources/PeerInfoStoryPaneNode.swift | 2 +- .../Sources/PlainButtonComponent.swift | 12 +- .../Sources/StoryAuthorInfoComponent.swift | 8 +- .../Sources/StoryAvatarInfoComponent.swift | 2 +- .../Sources/StoryContainerScreen.swift | 33 + .../Sources/StoryContent.swift | 3 + .../Sources/StoryItemContentComponent.swift | 7 + .../StoryItemSetContainerComponent.swift | 1009 ++++++++++------- ...StoryItemSetContainerViewSendMessage.swift | 225 ++-- .../Sources/StoryFooterPanelComponent.swift | 36 - .../Sources/StoryPeerListComponent.swift | 4 +- .../Stories/Close.imageset/Contents.json | 12 + .../Stories/Close.imageset/StoryClose.svg | 4 + .../Stories/SoundOff.imageset/Contents.json | 12 + .../SoundOff.imageset/StorySoundOff2.svg | 3 + .../Stories/SoundOn.imageset/Contents.json | 12 + .../Stories/SoundOn.imageset/StorySoundOn.svg | 3 + .../Sources/NativeVideoContent.swift | 4 +- 23 files changed, 893 insertions(+), 570 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/Close.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/Close.imageset/StoryClose.svg create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/SoundOff.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/SoundOff.imageset/StorySoundOff2.svg create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/SoundOn.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/SoundOn.imageset/StorySoundOn.svg diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 399ce1b9f0..06f91b8997 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -9,15 +9,16 @@ open class SparseNode: ASDisplayNode { if self.alpha.isZero { return nil } - if !self.bounds.inset(by: self.hitTestSlop).contains(point) { - return nil - } for view in self.view.subviews.reversed() { if let result = view.hitTest(self.view.convert(point, to: view), with: event), result.isUserInteractionEnabled { return result } } + if !self.bounds.inset(by: self.hitTestSlop).contains(point) { + return nil + } + let result = super.hitTest(point, with: event) if result != self.view { return result diff --git a/submodules/MediaPlayer/Sources/MediaPlayer.swift b/submodules/MediaPlayer/Sources/MediaPlayer.swift index 65329922cd..e1f07f6f70 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayer.swift @@ -413,7 +413,7 @@ private final class MediaPlayerContext { if let strongSelf = self { if strongSelf.enableSound { if strongSelf.continuePlayingWithoutSoundOnLostAudioSession { - strongSelf.continuePlayingWithoutSound() + strongSelf.continuePlayingWithoutSound(seek: .start) } else { strongSelf.pause(lostAudioSession: true, faded: false) } @@ -492,7 +492,7 @@ private final class MediaPlayerContext { if let strongSelf = self { if strongSelf.enableSound { if strongSelf.continuePlayingWithoutSoundOnLostAudioSession { - strongSelf.continuePlayingWithoutSound() + strongSelf.continuePlayingWithoutSound(seek: .start) } else { strongSelf.pause(lostAudioSession: true, faded: false) } @@ -578,7 +578,7 @@ private final class MediaPlayerContext { } else if let loadedState = loadedState, case .none = seek { timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) - if let duration = self.currentDuration() { + if let duration = self.currentDuration(), duration != 0.0 { if timestamp > duration - 2.0 { timestamp = 0.0 } @@ -622,7 +622,7 @@ private final class MediaPlayerContext { } } - fileprivate func continuePlayingWithoutSound() { + fileprivate func continuePlayingWithoutSound(seek: MediaPlayerSeek) { if self.enableSound { self.lastStatusUpdateTimestamp = nil @@ -647,10 +647,20 @@ private final class MediaPlayerContext { self.enableSound = false self.playAndRecord = false - var timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) - if let duration = self.currentDuration(), timestamp > duration - 2.0 { + var timestamp: Double + if case let .timecode(time) = seek { + timestamp = time + } else if case .none = seek { + timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) + if let duration = self.currentDuration(), duration != 0.0 { + if timestamp > duration - 2.0 { + timestamp = 0.0 + } + } + } else { timestamp = 0.0 } + self.seek(timestamp: timestamp, action: .play) } } @@ -1165,10 +1175,10 @@ public final class MediaPlayer { } } - public func continuePlayingWithoutSound() { + public func continuePlayingWithoutSound(seek: MediaPlayerSeek = .start) { self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { - context.continuePlayingWithoutSound() + context.continuePlayingWithoutSound(seek: seek) } } } diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 3bfd706e49..4dfdfd6062 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1354,10 +1354,8 @@ func debugRestoreState(basePath: String, name: String) { } } -private let sharedQueue = Queue(name: "org.telegram.postbox.Postbox") - public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, encryptionParameters: ValueBoxEncryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32, isTemporary: Bool, isReadOnly: Bool, useCopy: Bool, useCaches: Bool, removeDatabaseOnError: Bool) -> Signal { - let queue = sharedQueue + let queue = Postbox.sharedQueue return Signal { subscriber in queue.async { postboxLog("openPostbox, basePath: \(basePath), useCopy: \(useCopy)") @@ -4102,6 +4100,8 @@ final class PostboxImpl { } public class Postbox { + public static let sharedQueue = Queue(name: "org.telegram.postbox.Postbox") + let queue: Queue private let impl: QueueLocalObject diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift index b49037821e..c7cf01878f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift @@ -171,7 +171,8 @@ final class PendingStoryManager { var storyObserverContexts: [Int32: Bag<(Float) -> Void>] = [:] - private let allStoriesUploadProgressPromise = ValuePromise(nil, ignoreRepeated: true) + private let allStoriesUploadProgressPromise = Promise(nil) + private var allStoriesUploadProgressValue: Float? = nil var allStoriesUploadProgress: Signal { return self.allStoriesUploadProgressPromise.get() } @@ -291,7 +292,29 @@ final class PendingStoryManager { } private func processContextsUpdated() { - self.allStoriesUploadProgressPromise.set(self.currentPendingItemContext?.progress) + let currentProgress = self.currentPendingItemContext?.progress + if self.allStoriesUploadProgressValue != currentProgress { + let previousProgress = self.allStoriesUploadProgressValue + self.allStoriesUploadProgressValue = currentProgress + + if previousProgress != nil && currentProgress == nil { + // Hack: the UI is updated after 2 Postbox queries + let signal: Signal = Signal { subscriber in + Postbox.sharedQueue.justDispatch { + Postbox.sharedQueue.justDispatch { + subscriber.putNext(nil) + } + } + return EmptyDisposable + } + |> deliverOnMainQueue + + self.allStoriesUploadProgressPromise.set(signal) + } else { + self.allStoriesUploadProgressPromise.set(.single(currentProgress)) + } + } + self.hasPendingPromise.set(self.currentPendingItemContext != nil) } } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 33d4cef8ea..f2c7d2dc88 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -551,11 +551,7 @@ public final class MessageInputPanelComponent: Component { if component.attachmentAction != nil { let attachmentButtonMode: MessageInputActionButtonComponent.Mode - if !self.textFieldExternalState.isEditing && component.moreAction != nil { - attachmentButtonMode = .more - } else { - attachmentButtonMode = .attach - } + attachmentButtonMode = .attach let attachmentButtonSize = self.attachmentButton.update( transition: transition, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 73c8f528d2..6e310c5bf6 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1578,7 +1578,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr totalCount = state.totalCount totalCount = max(mappedItems.count, totalCount) - if totalCount == 0 && state.loadMoreToken != nil { + if totalCount == 0 && state.loadMoreToken != nil && !state.isCached { totalCount = 100 } diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift index 130b28a8e8..c984ddf456 100644 --- a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -12,15 +12,18 @@ public final class PlainButtonComponent: Component { public let content: AnyComponent public let effectAlignment: EffectAlignment + public let minSize: CGSize? public let action: () -> Void public init( content: AnyComponent, effectAlignment: EffectAlignment, + minSize: CGSize? = nil, action: @escaping () -> Void ) { self.content = content self.effectAlignment = effectAlignment + self.minSize = minSize self.action = action } @@ -31,6 +34,9 @@ public final class PlainButtonComponent: Component { if lhs.effectAlignment != rhs.effectAlignment { return false } + if lhs.minSize != rhs.minSize { + return false + } return true } @@ -122,7 +128,11 @@ public final class PlainButtonComponent: Component { containerSize: availableSize ) - let size = contentSize + var size = contentSize + if let minSize = component.minSize { + size.width = max(size.width, minSize.width) + size.height = max(size.height, minSize.height) + } if let contentView = self.content.view { var contentTransition = transition diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift index 8eb6609029..83a761618f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift @@ -73,20 +73,20 @@ final class StoryAuthorInfoComponent: Component { let titleSize = self.title.update( transition: .immediate, - component: AnyComponent(Text(text: title, font: Font.semibold(17.0), color: .white)), + component: AnyComponent(Text(text: title, font: Font.medium(14.0), color: .white)), environment: {}, containerSize: availableSize ) let subtitleSize = self.subtitle.update( transition: .immediate, - component: AnyComponent(Text(text: subtitle, font: Font.regular(12.0), color: UIColor(white: 1.0, alpha: 0.8))), + component: AnyComponent(Text(text: subtitle, font: Font.regular(11.0), color: UIColor(white: 1.0, alpha: 0.8))), environment: {}, containerSize: availableSize ) let contentHeight: CGFloat = titleSize.height + spacing + subtitleSize.height - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((availableSize.height - contentHeight) * 0.5)), size: titleSize) - let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + spacing), size: subtitleSize) + let titleFrame = CGRect(origin: CGPoint(x: 54.0, y: 2.0 + floor((availableSize.height - contentHeight) * 0.5)), size: titleSize) + let subtitleFrame = CGRect(origin: CGPoint(x: 54.0, y: titleFrame.maxY + spacing + UIScreenPixel), size: subtitleSize) if let titleView = self.title.view { if titleView.superview == nil { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAvatarInfoComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAvatarInfoComponent.swift index 314fcb4be3..86bf129c96 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAvatarInfoComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAvatarInfoComponent.swift @@ -48,7 +48,7 @@ final class StoryAvatarInfoComponent: Component { self.component = component self.state = state - let size = CGSize(width: 36.0, height: 36.0) + let size = CGSize(width: 32.0, height: 32.0) self.avatarNode.frame = CGRect(origin: CGPoint(), size: size) self.avatarNode.setPeer( diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 48550f5008..778b4d537f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -321,6 +321,8 @@ private final class StoryContainerScreenComponent: Component { private func commitHorizontalPan(velocity: CGPoint) { if var itemSetPanState = self.itemSetPanState { + var shouldDismiss = false + if let component = self.component, let stateValue = component.content.stateValue, let _ = stateValue.slice { var direction: StoryContentContextNavigation.PeerDirection? if abs(velocity.x) > 10.0 { @@ -345,6 +347,8 @@ private final class StoryContainerScreenComponent: Component { } self.itemSetPanState = itemSetPanState self.state?.updated(transition: .immediate) + } else { + shouldDismiss = true } } @@ -365,6 +369,10 @@ private final class StoryContainerScreenComponent: Component { component.content.resetSideStates() }*/ }) + + if shouldDismiss { + self.environment?.controller()?.dismiss() + } } } @@ -895,6 +903,31 @@ private final class StoryContainerScreenComponent: Component { }, controller: { [weak self] in return self?.environment?.controller() + }, + toggleAmbientMode: { [weak self] in + guard let self else { + return + } + + if self.storyItemSharedState.useAmbientMode { + self.storyItemSharedState.useAmbientMode = false + self.volumeButtonsListenerShouldBeActive.set(false) + + for (_, itemSetView) in self.visibleItemSetViews { + if let componentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { + componentView.leaveAmbientMode() + } + } + } else { + self.storyItemSharedState.useAmbientMode = true + self.volumeButtonsListenerShouldBeActive.set(true) + + for (_, itemSetView) in self.visibleItemSetViews { + if let componentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { + componentView.enterAmbientMode() + } + } + } } )), environment: {}, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index 4992da47cc..bb1b66aa7a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -30,6 +30,9 @@ public final class StoryContentItem: Equatable { open func leaveAmbientMode() { } + open func enterAmbientMode() { + } + open var videoPlaybackPosition: Double? { return nil } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 618e1f64da..b04b13c021 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -207,10 +207,17 @@ final class StoryItemContentComponent: Component { override func leaveAmbientMode() { if let videoNode = self.videoNode { + videoNode.setSoundEnabled(true) videoNode.continueWithOverridingAmbientMode() } } + override func enterAmbientMode() { + if let videoNode = self.videoNode { + videoNode.setSoundEnabled(false) + } + } + private func updateIsProgressPaused(update: Bool) { if let videoNode = self.videoNode { var canPlay = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 00de523710..e03fc13641 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -82,6 +82,7 @@ public final class StoryItemSetContainerComponent: Component { public let delete: () -> Void public let markAsSeen: (StoryId) -> Void public let controller: () -> ViewController? + public let toggleAmbientMode: () -> Void public init( context: AccountContext, @@ -106,7 +107,8 @@ public final class StoryItemSetContainerComponent: Component { navigate: @escaping (NavigationDirection) -> Void, delete: @escaping () -> Void, markAsSeen: @escaping (StoryId) -> Void, - controller: @escaping () -> ViewController? + controller: @escaping () -> ViewController?, + toggleAmbientMode: @escaping () -> Void ) { self.context = context self.externalState = externalState @@ -131,6 +133,7 @@ public final class StoryItemSetContainerComponent: Component { self.delete = delete self.markAsSeen = markAsSeen self.controller = controller + self.toggleAmbientMode = toggleAmbientMode } public static func ==(lhs: StoryItemSetContainerComponent, rhs: StoryItemSetContainerComponent) -> Bool { @@ -276,8 +279,10 @@ public final class StoryItemSetContainerComponent: Component { let navigationStrip = ComponentView() var centerInfoItem: InfoItem? - var rightInfoItem: InfoItem? + var leftInfoItem: InfoItem? + let moreButton = ComponentView() + let soundButton = ComponentView() var closeFriendIcon: ComponentView? var captionItem: CaptionItem? @@ -305,7 +310,6 @@ public final class StoryItemSetContainerComponent: Component { var reactionContextNode: ReactionContextNode? weak var disappearingReactionContextNode: ReactionContextNode? - weak var actionSheet: ActionSheetController? weak var contextController: ContextController? weak var privacyController: ShareWithPeersScreen? @@ -484,8 +488,8 @@ public final class StoryItemSetContainerComponent: Component { } } - if let rigthInfoItemView = self.rightInfoItem?.view.view { - if rigthInfoItemView.convert(rigthInfoItemView.bounds, to: self).contains(point) { + if let leftInfoItemView = self.leftInfoItem?.view.view { + if leftInfoItemView.convert(leftInfoItemView.bounds, to: self).contains(point) { return false } } @@ -526,6 +530,22 @@ public final class StoryItemSetContainerComponent: Component { if let itemView = visibleItem.view.view as? StoryContentItem.View { itemView.leaveAmbientMode() } + + self.state?.updated(transition: .immediate) + } + + func enterAmbientMode() { + guard let component = self.component else { + return + } + guard let visibleItem = self.visibleItems[component.slice.item.storyItem.id] else { + return + } + if let itemView = visibleItem.view.view as? StoryContentItem.View { + itemView.enterAmbientMode() + } + + self.state?.updated(transition: .immediate) } @objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { @@ -709,7 +729,7 @@ public final class StoryItemSetContainerComponent: Component { if component.pinchState != nil { return true } - if self.inputPanelExternalState.isEditing || component.isProgressPaused || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList { + if self.inputPanelExternalState.isEditing || component.isProgressPaused || self.sendMessageContext.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList { return true } if self.privacyController != nil { @@ -983,8 +1003,6 @@ public final class StoryItemSetContainerComponent: Component { } func animateIn(transitionIn: StoryContainerScreen.TransitionIn) { - self.closeButton.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, delay: 0.12, timingFunction: kCAMediaTimingFunctionSpring) - if let inputPanelView = self.inputPanel.view { inputPanelView.layer.animatePosition( from: CGPoint(x: 0.0, y: self.bounds.height - inputPanelView.frame.minY), @@ -1017,21 +1035,31 @@ public final class StoryItemSetContainerComponent: Component { } if let component = self.component, let sourceView = transitionIn.sourceView, let contentContainerView = self.visibleItems[component.slice.item.storyItem.id]?.contentContainerView { - let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self) - let innerSourceLocalFrame = CGRect(origin: CGPoint(x: sourceLocalFrame.minX - contentContainerView.frame.minX, y: sourceLocalFrame.minY - contentContainerView.frame.minY), size: sourceLocalFrame.size) - if let centerInfoView = self.centerInfoItem?.view.view { centerInfoView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } + if let moreButtonView = self.moreButton.view { + moreButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + if let soundButtonView = self.soundButton.view { + soundButtonView.layer.animateAlpha(from: 0.0, to: soundButtonView.alpha, duration: 0.25) + } + if let closeFriendIcon = self.closeFriendIcon?.view { + closeFriendIcon.layer.animateAlpha(from: 0.0, to: closeFriendIcon.alpha, duration: 0.25) + } + self.closeButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - if let rightInfoView = self.rightInfoItem?.view.view { + let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self) + let innerSourceLocalFrame = CGRect(origin: CGPoint(x: sourceLocalFrame.minX - contentContainerView.frame.minX, y: sourceLocalFrame.minY - contentContainerView.frame.minY), size: sourceLocalFrame.size) + + if let leftInfoView = self.leftInfoItem?.view.view { if transitionIn.sourceIsAvatar { - let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: CGPoint(x: innerSourceLocalFrame.center.x - rightInfoView.layer.position.x, y: innerSourceLocalFrame.center.y - rightInfoView.layer.position.y), to: CGPoint(), elevation: 0.0, duration: 0.3, curve: .spring, reverse: false) - rightInfoView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", additive: true) + let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: CGPoint(x: innerSourceLocalFrame.center.x - leftInfoView.layer.position.x, y: innerSourceLocalFrame.center.y - leftInfoView.layer.position.y), to: CGPoint(), elevation: 0.0, duration: 0.3, curve: .spring, reverse: false) + leftInfoView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", additive: true) - rightInfoView.layer.animateScale(from: innerSourceLocalFrame.width / rightInfoView.bounds.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + leftInfoView.layer.animateScale(from: innerSourceLocalFrame.width / leftInfoView.bounds.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) } else { - rightInfoView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + leftInfoView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } } @@ -1132,8 +1160,18 @@ public final class StoryItemSetContainerComponent: Component { if let centerInfoView = self.centerInfoItem?.view.view { centerInfoView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) } + if let moreButtonView = self.moreButton.view { + moreButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + if let soundButtonView = self.soundButton.view { + soundButtonView.layer.animateAlpha(from: soundButtonView.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + if let closeFriendIconView = self.closeFriendIcon?.view { + closeFriendIconView.layer.animateAlpha(from: closeFriendIconView.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + self.closeButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - if let rightInfoView = self.rightInfoItem?.view.view { + if let leftInfoView = self.leftInfoItem?.view.view { if transitionOut.destinationIsAvatar { let transitionView = transitionOut.transitionView @@ -1170,7 +1208,7 @@ public final class StoryItemSetContainerComponent: Component { } } - let rightInfoSourceFrame = rightInfoView.convert(rightInfoView.bounds, to: self) + let rightInfoSourceFrame = leftInfoView.convert(leftInfoView.bounds, to: self) let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: sourceLocalFrame.center, to: rightInfoSourceFrame.center, elevation: 0.0, duration: 0.3, curve: .spring, reverse: true) for transitionViewImpl in transitionViewsImpl { @@ -1190,7 +1228,7 @@ public final class StoryItemSetContainerComponent: Component { transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) } - rightInfoView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + leftInfoView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) for transitionViewImpl in transitionViewsImpl { transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) @@ -1209,11 +1247,11 @@ public final class StoryItemSetContainerComponent: Component { } } - let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: innerSourceLocalFrame.center, to: rightInfoView.layer.position, elevation: 0.0, duration: 0.3, curve: .spring, reverse: true) - rightInfoView.layer.position = positionKeyframes[positionKeyframes.count - 1] - rightInfoView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", removeOnCompletion: false, additive: false) + let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: innerSourceLocalFrame.center, to: leftInfoView.layer.position, elevation: 0.0, duration: 0.3, curve: .spring, reverse: true) + leftInfoView.layer.position = positionKeyframes[positionKeyframes.count - 1] + leftInfoView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", removeOnCompletion: false, additive: false) - rightInfoView.layer.animateScale(from: 1.0, to: innerSourceLocalFrame.width / rightInfoView.bounds.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + leftInfoView.layer.animateScale(from: 1.0, to: innerSourceLocalFrame.width / leftInfoView.bounds.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } } @@ -1523,134 +1561,17 @@ public final class StoryItemSetContainerComponent: Component { self.state?.updated(transition: .immediate) }, timeoutAction: nil, - forwardAction: component.slice.item.storyItem.isPublic && !component.slice.item.storyItem.isForwardingDisabled ? { [weak self] in + forwardAction: component.slice.item.storyItem.isPublic ? { [weak self] in guard let self else { return } self.sendMessageContext.performShareAction(view: self) } : nil, moreAction: { [weak self] sourceView, gesture in - guard let self, let component = self.component, let controller = component.controller() else { + guard let self else { return } - - component.controller()?.forEachController { c in - if let c = c as? UndoOverlayController { - c.dismiss() - } - return true - } - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - var items: [ContextMenuItem] = [] - - let isMuted = component.slice.additionalPeerData.isMuted - items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Not Notify", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: component.slice.additionalPeerData.isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return - } - - let _ = component.context.engine.peers.togglePeerStoriesMuted(peerId: component.slice.peer.id).start() - - let iconColor = UIColor.white - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - if isMuted { - self.component?.presentController(UndoOverlayController( - presentationData: presentationData, - content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [ - "Middle.Group 1.Fill 1": iconColor, - "Top.Group 1.Fill 1": iconColor, - "Bottom.Group 1.Fill 1": iconColor, - "EXAMPLE.Group 1.Fill 1": iconColor, - "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: "You will now get a notification whenever **\(component.slice.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))** posts a story.", customUndoText: nil, timeout: nil), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), nil) - } else { - self.component?.presentController(UndoOverlayController( - presentationData: presentationData, - content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [ - "Middle.Group 1.Fill 1": iconColor, - "Top.Group 1.Fill 1": iconColor, - "Bottom.Group 1.Fill 1": iconColor, - "EXAMPLE.Group 1.Fill 1": iconColor, - "Line.Group 1.Stroke 1": iconColor - ], title: nil, text: "You will no longer receive a notification when **\(component.slice.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))** posts a story.", customUndoText: nil, timeout: nil), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), nil) - } - }))) - - var isHidden = false - if case let .user(user) = component.slice.peer, let storiesHidden = user.storiesHidden { - isHidden = storiesHidden - } - - items.append(.action(ContextMenuActionItem(text: isHidden ? "Unhide \(component.slice.peer.compactDisplayTitle)" : "Hide \(component.slice.peer.compactDisplayTitle)", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: isHidden ? "Chat/Context Menu/MoveToChats" : "Chat/Context Menu/MoveToContacts"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return - } - - let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: !isHidden) - }))) - - items.append(.action(ContextMenuActionItem(text: "Report", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, a in - guard let self, let component = self.component, let controller = component.controller() else { - return - } - - let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] - presentPeerReportOptions( - context: component.context, - parent: controller, - contextController: c, - backAction: { _ in }, - subject: .story(component.slice.peer.id, component.slice.item.storyItem.id), - options: options, - passthrough: true, - forceTheme: defaultDarkPresentationTheme, - isDetailedReportingVisible: { [weak self] isReporting in - guard let self else { - return - } - self.isReporting = isReporting - self.updateIsProgressPaused() - }, - completion: { [weak self] reason, _ in - guard let self, let component = self.component, let controller = component.controller(), let reason else { - return - } - let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.peer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").start() - controller.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) - } - ) - }))) - - let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(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)) + self.performMoreAction(sourceView: sourceView, gesture: gesture) }, presentVoiceMessagesUnavailableTooltip: { [weak self] view in guard let self, let component = self.component, self.voiceMessagesRestrictedTooltipController == nil else { @@ -1807,26 +1728,6 @@ public final class StoryItemSetContainerComponent: Component { return } component.delete() - - /*if let currentSlice = self.currentSlice, let index = currentSlice.items.firstIndex(where: { $0.id == focusedItemId }) { - let item = currentSlice.items[index] - - if currentSlice.items.count == 1 { - component.navigateToItemSet(.next) - } else { - var nextIndex: Int = index + 1 - if nextIndex >= currentSlice.items.count { - nextIndex = currentSlice.items.count - 1 - } - self.focusedItemId = currentSlice.items[nextIndex].id - - currentSlice.items[nextIndex].markAsSeen?() - - self.state?.updated(transition: .immediate) - } - - item.delete?() - }*/ }) ]), ActionSheetItemGroup(items: [ @@ -1840,179 +1741,19 @@ public final class StoryItemSetContainerComponent: Component { guard let self else { return } - self.actionSheet = nil + self.sendMessageContext.actionSheet = nil self.updateIsProgressPaused() } - self.actionSheet = actionSheet + self.sendMessageContext.actionSheet = actionSheet self.updateIsProgressPaused() component.presentController(actionSheet, nil) }, moreAction: { [weak self] sourceView, gesture in - guard let self, let component = self.component, let controller = component.controller() else { + guard let self else { return } - - component.controller()?.forEachController { c in - if let c = c as? UndoOverlayController { - c.dismiss() - } - return true - } - - var items: [ContextMenuItem] = [] - - let additionalCount = component.slice.item.storyItem.privacy?.additionallyIncludePeers.count ?? 0 - - let privacyText: String - switch component.slice.item.storyItem.privacy?.base { - case .closeFriends: - privacyText = "Close Friends" - case .contacts: - if additionalCount != 0 { - privacyText = "Contacts (-\(additionalCount))" - } else { - privacyText = "Contacts" - } - case .nobody: - if additionalCount != 0 { - if additionalCount == 1 { - privacyText = "\(additionalCount) Person" - } else { - privacyText = "\(additionalCount) People" - } - } else { - privacyText = "Only Me" - } - default: - privacyText = "Everyone" - } - - items.append(.action(ContextMenuActionItem(text: "Who can see", textLayout: .secondLineWithValue(privacyText), icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - self.openItemPrivacySettings() - }))) - - items.append(.action(ContextMenuActionItem(text: "Edit Story", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - self.openStoryEditing() - }))) - - items.append(.separator) - - items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from profile" : "Save to profile", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Chat/Context Menu/Check" : "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return - } - - let _ = component.context.engine.messages.updateStoriesArePinned(ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).start() - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - if component.slice.item.storyItem.isPinned { - self.component?.presentController(UndoOverlayController( - presentationData: presentationData, - content: .info(title: nil, text: "Story removed from your profile", timeout: nil), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), nil) - } else { - self.component?.presentController(UndoOverlayController( - presentationData: presentationData, - content: .info(title: "Story saved to your profile", text: "Saved stories can be viewed by others on your profile until you remove them.", timeout: nil), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), nil) - } - }))) - - let saveText: String - if case .file = component.slice.item.storyItem.media { - saveText = "Save Video" - } else { - saveText = "Save Image" - } - items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - self.requestSave() - }))) - - if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) { - items.append(.action(ContextMenuActionItem(text: "Copy link", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return - } - - let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id) - |> deliverOnMainQueue).start(next: { [weak self] link in - guard let self, let component = self.component else { - return - } - if let link { - UIPasteboard.general.string = link - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - component.presentController(UndoOverlayController( - presentationData: presentationData, - content: .linkCopied(text: "Link copied."), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), nil) - } - }) - }))) - items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - self.sendMessageContext.performShareAction(view: self) - }))) - } - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(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)) + self.performMoreAction(sourceView: sourceView, gesture: gesture) }, openPeer: { [weak self] peer in guard let self else { @@ -2055,7 +1796,10 @@ public final class StoryItemSetContainerComponent: Component { let contentVisualBottomInset: CGFloat = max(contentDefaultBottomInset, viewListInset) - let contentVisualHeight = min(contentSize.height, availableSize.height - component.containerInsets.top - contentVisualBottomInset) + var contentVisualHeight = min(contentSize.height, availableSize.height - component.containerInsets.top - contentVisualBottomInset) + if contentVisualHeight < contentSize.height && contentVisualHeight >= contentSize.height - 5 { + contentVisualHeight = contentSize.height + } let contentVisualScale = min(1.0, contentVisualHeight / contentSize.height) let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top - (contentSize.height - contentVisualHeight) * 0.5), size: contentSize) @@ -2090,37 +1834,232 @@ public final class StoryItemSetContainerComponent: Component { transition.setCornerRadius(layer: self.controlsContainerView.layer, cornerRadius: 12.0 * (1.0 / contentVisualScale)) + var headerRightOffset: CGFloat = availableSize.width + if self.closeButtonIconView.image == nil { - self.closeButtonIconView.image = UIImage(bundleImageName: "Media Gallery/Close")?.withRenderingMode(.alwaysTemplate) + self.closeButtonIconView.image = UIImage(bundleImageName: "Stories/Close")?.withRenderingMode(.alwaysTemplate) self.closeButtonIconView.tintColor = .white } if let image = self.closeButtonIconView.image { - let closeButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 50.0, height: 64.0)) + let closeButtonFrame = CGRect(origin: CGPoint(x: headerRightOffset - 50.0, y: 2.0), size: CGSize(width: 50.0, height: 64.0)) transition.setFrame(view: self.closeButton, frame: closeButtonFrame) transition.setFrame(view: self.closeButtonIconView, frame: CGRect(origin: CGPoint(x: floor((closeButtonFrame.width - image.size.width) * 0.5), y: floor((closeButtonFrame.height - image.size.height) * 0.5)), size: image.size)) + headerRightOffset -= 51.0 + } + + let moreButtonSize = self.moreButton.update( + transition: transition, + component: AnyComponent(MessageInputActionButtonComponent( + mode: .more, + action: { _, _, _ in + }, + switchMediaInputMode: { + }, + updateMediaCancelFraction: { _ in + }, + lockMediaRecording: { + }, + stopAndPreviewMediaRecording: { + }, + moreAction: { [weak self] view, gesture in + guard let self else { + return + } + self.performMoreAction(sourceView: view, gesture: gesture) + }, + context: component.context, + theme: component.theme, + strings: component.strings, + presentController: { [weak self] c in + guard let self, let component = self.component else { + return + } + component.presentController(c, nil) + }, + audioRecorder: nil, + videoRecordingStatus: nil + )), + environment: {}, + containerSize: CGSize(width: 33.0, height: 64.0) + ) + if let moreButtonView = self.moreButton.view { + if moreButtonView.superview == nil { + self.controlsContainerView.addSubview(moreButtonView) + } + transition.setFrame(view: moreButtonView, frame: CGRect(origin: CGPoint(x: headerRightOffset - moreButtonSize.width, y: 2.0), size: moreButtonSize)) + headerRightOffset -= moreButtonSize.width + 15.0 + } + + var isSilentVideo = false + var isVideo = false + var soundAlpha: CGFloat = 0.0 + if case let .file(file) = component.slice.item.storyItem.media { + isVideo = true + soundAlpha = 1.0 + for attribute in file.attributes { + if case let .Video(_, _, flags, _) = attribute { + if flags.contains(.isSilent) { + isSilentVideo = true + soundAlpha = 0.5 + } + } + } + } + + let soundImage: String + if isSilentVideo || component.storyItemSharedState.useAmbientMode { + soundImage = "Stories/SoundOff" + } else { + soundImage = "Stories/SoundOn" + } + + let soundButtonSize = self.soundButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(BundleIconComponent( + name: soundImage, + tintColor: .white, + maxSize: nil + )), + effectAlignment: .center, + minSize: CGSize(width: 33.0, height: 64.0), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + var isSilentVideo = false + if case let .file(file) = component.slice.item.storyItem.media { + for attribute in file.attributes { + if case let .Video(_, _, flags, _) = attribute { + if flags.contains(.isSilent) { + isSilentVideo = true + } + } + } + } + + if isSilentVideo { + guard let soundButtonView = self.soundButton.view else { + return + } + let tooltipScreen = TooltipScreen( + account: component.context.account, + sharedContext: component.context.sharedContext, + text: "This video has no sound", style: .default, location: TooltipScreen.Location.point(soundButtonView.convert(soundButtonView.bounds, to: self).offsetBy(dx: 1.0, dy: -10.0), .top), displayDuration: .manual(true), shouldDismissOnTouch: { _ in + return .dismiss(consume: true) + } + ) + tooltipScreen.willBecomeDismissed = { [weak self] _ in + guard let self else { + return + } + self.sendMessageContext.tooltipScreen = nil + self.updateIsProgressPaused() + } + self.sendMessageContext.tooltipScreen = tooltipScreen + self.updateIsProgressPaused() + component.controller()?.present(tooltipScreen, in: .current) + } else { + component.toggleAmbientMode() + } + } + )), + environment: {}, + containerSize: CGSize(width: 33.0, height: 64.0) + ) + + if let soundButtonView = self.soundButton.view { + if soundButtonView.superview == nil { + self.controlsContainerView.addSubview(soundButtonView) + } + transition.setFrame(view: soundButtonView, frame: CGRect(origin: CGPoint(x: headerRightOffset - soundButtonSize.width, y: 2.0), size: soundButtonSize)) + transition.setAlpha(view: soundButtonView, alpha: soundAlpha) + + if isVideo { + headerRightOffset -= soundButtonSize.width + 16.0 + } + } + + if component.slice.item.storyItem.isCloseFriends && component.slice.peer.id != component.context.account.peerId { + let closeFriendIcon: ComponentView + var closeFriendIconTransition = transition + if let current = self.closeFriendIcon { + closeFriendIcon = current + } else { + closeFriendIconTransition = .immediate + closeFriendIcon = ComponentView() + self.closeFriendIcon = closeFriendIcon + } + let closeFriendIconSize = closeFriendIcon.update( + transition: closeFriendIconTransition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(BundleIconComponent( + name: "Stories/CloseStoryIcon", + tintColor: nil, + maxSize: nil + )), + effectAlignment: .center, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + guard let closeFriendIconView = self.closeFriendIcon?.view else { + return + } + let tooltipScreen = TooltipScreen( + account: component.context.account, + sharedContext: component.context.sharedContext, + text: "You are seeing this story because you have\nbeen added to \(component.slice.peer.compactDisplayTitle)'s list of close friends.", style: .default, location: TooltipScreen.Location.point(closeFriendIconView.convert(closeFriendIconView.bounds, to: self).offsetBy(dx: 1.0, dy: 6.0), .top), displayDuration: .manual(true), shouldDismissOnTouch: { _ in + return .dismiss(consume: true) + } + ) + tooltipScreen.willBecomeDismissed = { [weak self] _ in + guard let self else { + return + } + self.sendMessageContext.tooltipScreen = nil + self.updateIsProgressPaused() + } + self.sendMessageContext.tooltipScreen = tooltipScreen + self.updateIsProgressPaused() + component.controller()?.present(tooltipScreen, in: .current) + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let closeFriendIconFrame = CGRect(origin: CGPoint(x: headerRightOffset - closeFriendIconSize.width - 8.0, y: 23.0), size: closeFriendIconSize) + if let closeFriendIconView = closeFriendIcon.view { + if closeFriendIconView.superview == nil { + self.controlsContainerView.addSubview(closeFriendIconView) + } + + closeFriendIconTransition.setFrame(view: closeFriendIconView, frame: closeFriendIconFrame) + headerRightOffset -= 44.0 + } + } else if let closeFriendIcon = self.closeFriendIcon { + self.closeFriendIcon = nil + closeFriendIcon.view?.removeFromSuperview() } transition.setAlpha(view: self.controlsContainerView, alpha: (component.hideUI || self.isEditingStory || self.displayViewList) ? 0.0 : 1.0) let focusedItem: StoryContentItem? = component.slice.item let _ = focusedItem - /*if let currentSlice = self.currentSlice, let item = currentSlice.items.first(where: { $0.id == self.focusedItemId }) { - focusedItem = item - }*/ - var currentRightInfoItem: InfoItem? + var currentLeftInfoItem: InfoItem? if focusedItem != nil { - let rightInfoComponent = AnyComponent(StoryAvatarInfoComponent(context: component.context, peer: component.slice.peer)) - if let rightInfoItem = self.rightInfoItem, rightInfoItem.component == rightInfoComponent { - currentRightInfoItem = rightInfoItem + let leftInfoComponent = AnyComponent(StoryAvatarInfoComponent(context: component.context, peer: component.slice.peer)) + if let leftInfoItem = self.leftInfoItem, leftInfoItem.component == leftInfoComponent { + currentLeftInfoItem = leftInfoItem } else { - currentRightInfoItem = InfoItem(component: rightInfoComponent) + currentLeftInfoItem = InfoItem(component: leftInfoComponent) } } - if let rightInfoItem = self.rightInfoItem, currentRightInfoItem?.component != rightInfoItem.component { - self.rightInfoItem = nil - if let view = rightInfoItem.view.view { + if let leftInfoItem = self.leftInfoItem, currentLeftInfoItem?.component != leftInfoItem.component { + self.leftInfoItem = nil + if let view = leftInfoItem.view.view { view.layer.animateScale(from: 1.0, to: 0.5, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in view?.removeFromSuperview() @@ -2175,27 +2114,27 @@ public final class StoryItemSetContainerComponent: Component { } } - if let currentRightInfoItem { - self.rightInfoItem = currentRightInfoItem + if let currentLeftInfoItem { + self.leftInfoItem = currentLeftInfoItem - let rightInfoItemSize = currentRightInfoItem.view.update( + let leftInfoItemSize = currentLeftInfoItem.view.update( transition: .immediate, - component: AnyComponent(PlainButtonComponent(content: currentRightInfoItem.component, effectAlignment: .center, action: { [weak self] in + component: AnyComponent(PlainButtonComponent(content: currentLeftInfoItem.component, effectAlignment: .center, action: { [weak self] in guard let self, let component = self.component else { return } self.navigateToPeer(peer: component.slice.peer) })), environment: {}, - containerSize: CGSize(width: 36.0, height: 36.0) + containerSize: CGSize(width: 32.0, height: 32.0) ) - if let view = currentRightInfoItem.view.view { + if let view = currentLeftInfoItem.view.view { var animateIn = false if view.superview == nil { self.controlsContainerView.addSubview(view) animateIn = true } - transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: contentFrame.width - 6.0 - rightInfoItemSize.width, y: 14.0), size: rightInfoItemSize)) + transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: 12.0, y: 18.0), size: leftInfoItemSize)) if animateIn, !isFirstTime, !transition.animation.isImmediate { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -2206,66 +2145,6 @@ public final class StoryItemSetContainerComponent: Component { } } - if component.slice.item.storyItem.isCloseFriends && component.slice.peer.id != component.context.account.peerId { - let closeFriendIcon: ComponentView - var closeFriendIconTransition = transition - if let current = self.closeFriendIcon { - closeFriendIcon = current - } else { - closeFriendIconTransition = .immediate - closeFriendIcon = ComponentView() - self.closeFriendIcon = closeFriendIcon - } - let closeFriendIconSize = closeFriendIcon.update( - transition: closeFriendIconTransition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(BundleIconComponent( - name: "Stories/CloseStoryIcon", - tintColor: nil, - maxSize: nil - )), - effectAlignment: .center, - action: { [weak self] in - guard let self, let component = self.component else { - return - } - guard let closeFriendIconView = self.closeFriendIcon?.view else { - return - } - let tooltipScreen = TooltipScreen( - account: component.context.account, - sharedContext: component.context.sharedContext, - text: "You are seeing this story because you have\nbeen added to \(component.slice.peer.compactDisplayTitle)'s list of close friends.", style: .default, location: TooltipScreen.Location.point(closeFriendIconView.convert(closeFriendIconView.bounds, to: self).offsetBy(dx: 1.0, dy: 6.0), .top), displayDuration: .manual(true), shouldDismissOnTouch: { _ in - return .dismiss(consume: false) - } - ) - tooltipScreen.willBecomeDismissed = { [weak self] _ in - guard let self else { - return - } - self.sendMessageContext.tooltipScreen = nil - self.updateIsProgressPaused() - } - self.sendMessageContext.tooltipScreen = tooltipScreen - self.updateIsProgressPaused() - component.controller()?.present(tooltipScreen, in: .current) - } - )), - environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) - ) - let closeFriendIconFrame = CGRect(origin: CGPoint(x: contentFrame.width - 6.0 - 52.0 - closeFriendIconSize.width, y: 21.0), size: closeFriendIconSize) - if let closeFriendIconView = closeFriendIcon.view { - if closeFriendIconView.superview == nil { - self.controlsContainerView.addSubview(closeFriendIconView) - closeFriendIconTransition.setFrame(view: closeFriendIconView, frame: closeFriendIconFrame) - } - } - } else if let closeFriendIcon = self.closeFriendIcon { - self.closeFriendIcon = nil - closeFriendIcon.view?.removeFromSuperview() - } - let gradientHeight: CGFloat = 74.0 transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width, height: gradientHeight))) transition.setAlpha(layer: self.topContentGradientLayer, alpha: (component.hideUI || self.displayViewList || self.isEditingStory) ? 0.0 : 1.0) @@ -2796,19 +2675,17 @@ public final class StoryItemSetContainerComponent: Component { guard let navigationController = controller.navigationController as? NavigationController else { return } + guard let chatController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + return + } - component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: messageId.flatMap { .message(id: .id($0), highlight: false, timecode: nil) }, keepStack: .always, animated: true, pushController: { [weak controller, weak navigationController] chatController, animated, completion in - guard let controller, let navigationController else { - return - } - var viewControllers = navigationController.viewControllers - if let index = viewControllers.firstIndex(where: { $0 === controller }) { - viewControllers.insert(chatController, at: index) - } else { - viewControllers.append(chatController) - } - navigationController.setViewControllers(viewControllers, animated: animated) - })) + var viewControllers = navigationController.viewControllers + if let index = viewControllers.firstIndex(where: { $0 === controller }) { + viewControllers.insert(chatController, at: index) + } else { + viewControllers.append(chatController) + } + navigationController.setViewControllers(viewControllers, animated: true) controller.dismissWithoutTransitionOut() } @@ -3025,6 +2902,296 @@ public final class StoryItemSetContainerComponent: Component { disposable.dispose() } } + + private func performMoreAction(sourceView: UIView, gesture: ContextGesture?) { + guard let component = self.component else { + return + } + if component.slice.peer.id == component.context.account.peerId { + self.performMyMoreAction(sourceView: sourceView, gesture: gesture) + } else { + self.performOtherMoreAction(sourceView: sourceView, gesture: gesture) + } + } + + private func performMyMoreAction(sourceView: UIView, gesture: ContextGesture?) { + guard let component = self.component, let controller = component.controller() else { + return + } + + component.controller()?.forEachController { c in + if let c = c as? UndoOverlayController { + c.dismiss() + } + return true + } + + var items: [ContextMenuItem] = [] + + let additionalCount = component.slice.item.storyItem.privacy?.additionallyIncludePeers.count ?? 0 + + let privacyText: String + switch component.slice.item.storyItem.privacy?.base { + case .closeFriends: + privacyText = "Close Friends" + case .contacts: + if additionalCount != 0 { + privacyText = "Contacts (-\(additionalCount))" + } else { + privacyText = "Contacts" + } + case .nobody: + if additionalCount != 0 { + if additionalCount == 1 { + privacyText = "\(additionalCount) Person" + } else { + privacyText = "\(additionalCount) People" + } + } else { + privacyText = "Only Me" + } + default: + privacyText = "Everyone" + } + + items.append(.action(ContextMenuActionItem(text: "Who can see", textLayout: .secondLineWithValue(privacyText), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.openItemPrivacySettings() + }))) + + items.append(.action(ContextMenuActionItem(text: "Edit Story", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.openStoryEditing() + }))) + + items.append(.separator) + + items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from profile" : "Save to profile", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Chat/Context Menu/Check" : "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.messages.updateStoriesArePinned(ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).start() + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + if component.slice.item.storyItem.isPinned { + self.component?.presentController(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: "Story removed from your profile", timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), nil) + } else { + self.component?.presentController(UndoOverlayController( + presentationData: presentationData, + content: .info(title: "Story saved to your profile", text: "Saved stories can be viewed by others on your profile until you remove them.", timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), nil) + } + }))) + + let saveText: String + if case .file = component.slice.item.storyItem.media { + saveText = "Save Video" + } else { + saveText = "Save Image" + } + items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.requestSave() + }))) + + if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) { + items.append(.action(ContextMenuActionItem(text: "Copy link", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.sendMessageContext.performCopyLinkAction(view: self) + }))) + items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.sendMessageContext.performShareAction(view: self) + }))) + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(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 performOtherMoreAction(sourceView: UIView, gesture: ContextGesture?) { + guard let component = self.component else { + return + } + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: component.slice.peer.id)) + |> deliverOnMainQueue).start(next: { [weak self] settings in + guard let self, let component = self.component, let controller = component.controller() else { + return + } + + component.controller()?.forEachController { c in + if let c = c as? UndoOverlayController { + c.dismiss() + } + return true + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + var items: [ContextMenuItem] = [] + + let isMuted = settings.storiesMuted == true + items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Not Notify", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: component.slice.additionalPeerData.isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.peers.togglePeerStoriesMuted(peerId: component.slice.peer.id).start() + + let iconColor = UIColor.white + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + if isMuted { + self.component?.presentController(UndoOverlayController( + presentationData: presentationData, + content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [ + "Middle.Group 1.Fill 1": iconColor, + "Top.Group 1.Fill 1": iconColor, + "Bottom.Group 1.Fill 1": iconColor, + "EXAMPLE.Group 1.Fill 1": iconColor, + "Line.Group 1.Stroke 1": iconColor + ], title: nil, text: "You will now get a notification whenever **\(component.slice.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))** posts a story.", customUndoText: nil, timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), nil) + } else { + self.component?.presentController(UndoOverlayController( + presentationData: presentationData, + content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [ + "Middle.Group 1.Fill 1": iconColor, + "Top.Group 1.Fill 1": iconColor, + "Bottom.Group 1.Fill 1": iconColor, + "EXAMPLE.Group 1.Fill 1": iconColor, + "Line.Group 1.Stroke 1": iconColor + ], title: nil, text: "You will no longer receive a notification when **\(component.slice.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))** posts a story.", customUndoText: nil, timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), nil) + } + }))) + + var isHidden = false + if case let .user(user) = component.slice.peer, let storiesHidden = user.storiesHidden { + isHidden = storiesHidden + } + + items.append(.action(ContextMenuActionItem(text: isHidden ? "Unhide \(component.slice.peer.compactDisplayTitle)" : "Hide \(component.slice.peer.compactDisplayTitle)", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: isHidden ? "Chat/Context Menu/MoveToChats" : "Chat/Context Menu/MoveToContacts"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: !isHidden) + }))) + + items.append(.action(ContextMenuActionItem(text: "Report", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, a in + guard let self, let component = self.component, let controller = component.controller() else { + return + } + + let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] + presentPeerReportOptions( + context: component.context, + parent: controller, + contextController: c, + backAction: { _ in }, + subject: .story(component.slice.peer.id, component.slice.item.storyItem.id), + options: options, + passthrough: true, + forceTheme: defaultDarkPresentationTheme, + isDetailedReportingVisible: { [weak self] isReporting in + guard let self else { + return + } + self.isReporting = isReporting + self.updateIsProgressPaused() + }, + completion: { [weak self] reason, _ in + guard let self, let component = self.component, let controller = component.controller(), let reason else { + return + } + let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.peer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").start() + controller.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) + } + ) + }))) + + let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(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)) + }) + } } public func makeView() -> View { @@ -3049,7 +3216,7 @@ private final class HeaderContextReferenceContentSource: ContextReferenceContent } func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top) + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .bottom) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 00552c3bc0..203af9a4d7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -49,6 +49,7 @@ final class StoryItemSetContainerSendMessage { weak var attachmentController: AttachmentController? weak var shareController: ShareController? weak var tooltipScreen: ViewController? + weak var actionSheet: ViewController? var currentInputMode: InputMode = .text private var needsInputActivation = false @@ -640,103 +641,163 @@ final class StoryItemSetContainerSendMessage { return } - var preferredAction: ShareControllerPreferredAction? - if focusedItem.storyItem.isPublic { - preferredAction = .custom(action: ShareControllerAction(title: "Copy Link", action: { - let _ = ((component.context.engine.messages.exportStoryLink(peerId: peerId, id: focusedItem.storyItem.id)) - |> deliverOnMainQueue).start(next: { link in - if let link { - UIPasteboard.general.string = link + if focusedItem.storyItem.isForwardingDisabled { + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Copy Link", color: .accent, action: { [weak self, weak view, weak actionSheet] in + actionSheet?.dismissAnimated() - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - component.presentController(UndoOverlayController( - presentationData: presentationData, - content: .linkCopied(text: "Link copied."), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), nil) - } - }) - })) - } - - let shareController = ShareController( - context: component.context, - subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id)))), - preferredAction: preferredAction ?? .default, - externalShare: false, - immediateExternalShare: false, - forceTheme: defaultDarkColorPresentationTheme - ) - - shareController.completed = { [weak view] peerIds in - guard let view, let component = view.component else { - return + guard let self, let view else { + return + } + self.performCopyLinkAction(view: view) + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + + actionSheet.dismissed = { [weak self, weak view] _ in + guard let self, let view else { + return + } + self.actionSheet = nil + view.updateIsProgressPaused() + } + self.actionSheet = actionSheet + view.updateIsProgressPaused() + + component.presentController(actionSheet, nil) + } else { + var preferredAction: ShareControllerPreferredAction? + if focusedItem.storyItem.isPublic { + preferredAction = .custom(action: ShareControllerAction(title: "Copy Link", action: { + let _ = ((component.context.engine.messages.exportStoryLink(peerId: peerId, id: focusedItem.storyItem.id)) + |> deliverOnMainQueue).start(next: { link in + if let link { + UIPasteboard.general.string = link + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + component.presentController(UndoOverlayController( + presentationData: presentationData, + content: .linkCopied(text: "Link copied."), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), nil) + } + }) + })) } - let _ = (component.context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) + let shareController = ShareController( + context: component.context, + subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id)))), + preferredAction: preferredAction ?? .default, + externalShare: false, + immediateExternalShare: false, + forceTheme: defaultDarkColorPresentationTheme ) - |> deliverOnMainQueue).start(next: { [weak view] peerList in + + shareController.completed = { [weak view] peerIds in guard let view, let component = view.component else { return } - let peers = peerList.compactMap { $0 } - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let text: String - var savedMessages = false - if peerIds.count == 1, let peerId = peerIds.first, peerId == component.context.account.peerId { - text = presentationData.strings.Conversation_StoryForwardTooltip_SavedMessages_One - savedMessages = true - } else { - if peers.count == 1, let peer = peers.first { - var peerName = peer.id == component.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_StoryForwardTooltip_Chat_One(peerName).string - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - var firstPeerName = firstPeer.id == component.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") - var secondPeerName = secondPeer.id == component.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_StoryForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_StoryForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string - } else { - text = "" + let _ = (component.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).start(next: { [weak view] peerList in + guard let view, let component = view.component else { + return } - } - - if let controller = component.controller() { + + let peers = peerList.compactMap { $0 } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController( - presentationData: presentationData, - content: .forward(savedMessages: savedMessages, text: text), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), in: .current) + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == component.context.account.peerId { + text = presentationData.strings.Conversation_StoryForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + var peerName = peer.id == component.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_StoryForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + var firstPeerName = firstPeer.id == component.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == component.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_StoryForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_StoryForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + if let controller = component.controller() { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController( + presentationData: presentationData, + content: .forward(savedMessages: savedMessages, text: text), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), in: .current) + } + }) + } + + self.shareController = shareController + view.updateIsProgressPaused() + + shareController.dismissed = { [weak self, weak view] _ in + guard let self, let view else { + return } - }) + self.shareController = nil + view.updateIsProgressPaused() + } + + controller.present(shareController, in: .window(.root)) + } + } + + func performCopyLinkAction(view: StoryItemSetContainerComponent.View) { + guard let component = view.component else { + return } - self.shareController = shareController - view.updateIsProgressPaused() - - shareController.dismissed = { [weak self, weak view] _ in - guard let self, let view else { + let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id) + |> deliverOnMainQueue).start(next: { [weak view] link in + guard let view, let component = view.component else { return } - self.shareController = nil - view.updateIsProgressPaused() - } - - controller.present(shareController, in: .window(.root)) + if let link { + UIPasteboard.general.string = link + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + component.presentController(UndoOverlayController( + presentationData: presentationData, + content: .linkCopied(text: "Link copied."), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), nil) + } + }) } private func clearInputText(view: StoryItemSetContainerComponent.View) { diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 1eaaefa2b0..2efabc8e71 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -53,7 +53,6 @@ public final class StoryFooterPanelComponent: Component { private let viewStatsText = ComponentView() private let viewStatsExpandedText = ComponentView() private let deleteButton = ComponentView() - private var moreButton: MoreHeaderButton? private var statusButton: HighlightableButton? private var statusNode: SemanticStatusNode? @@ -322,41 +321,6 @@ public final class StoryFooterPanelComponent: Component { transition.setAlpha(view: deleteButtonView, alpha: pow(1.0 - component.expandFraction, 1.0) * baseViewCountAlpha) } - let moreButton: MoreHeaderButton - if let current = self.moreButton { - moreButton = current - } else { - if let moreButton = self.moreButton { - moreButton.removeFromSupernode() - self.moreButton = nil - } - - moreButton = MoreHeaderButton(color: .white) - moreButton.isUserInteractionEnabled = true - moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: .white))) - moreButton.onPressed = { [weak self] in - guard let self, let component = self.component, let moreButton = self.moreButton else { - return - } - moreButton.play() - component.moreAction(moreButton.view, nil) - } - moreButton.contextAction = { [weak self] sourceNode, gesture in - guard let self, let component = self.component, let moreButton = self.moreButton else { - return - } - moreButton.play() - component.moreAction(moreButton.view, gesture) - } - self.moreButton = moreButton - self.addSubnode(moreButton) - } - - let buttonSize = CGSize(width: 32.0, height: 44.0) - moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: .white))) - transition.setFrame(view: moreButton.view, frame: CGRect(origin: CGPoint(x: rightContentOffset - buttonSize.width, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize)) - transition.setAlpha(view: moreButton.view, alpha: pow(1.0 - component.expandFraction, 1.0) * baseViewCountAlpha) - return size } } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index e43b5f739c..9e08b47168 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -541,7 +541,9 @@ public final class StoryPeerListComponent: Component { if component.useHiddenList { collapseStartIndex = 0 } else if let storySubscriptions = component.storySubscriptions { - if let accountItem = storySubscriptions.accountItem, (accountItem.hasUnseen || accountItem.hasPending) { + if self.sortedItems.count < 3, let accountItem = storySubscriptions.accountItem, accountItem.storyCount != 0 { + collapseStartIndex = 1 + } else if let accountItem = storySubscriptions.accountItem, (accountItem.hasUnseen || accountItem.hasPending) { collapseStartIndex = 0 } else { collapseStartIndex = 1 diff --git a/submodules/TelegramUI/Images.xcassets/Stories/Close.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/Close.imageset/Contents.json new file mode 100644 index 0000000000..d69c9e1ac5 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/Close.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "StoryClose.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/Close.imageset/StoryClose.svg b/submodules/TelegramUI/Images.xcassets/Stories/Close.imageset/StoryClose.svg new file mode 100644 index 0000000000..596f8796ce --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/Close.imageset/StoryClose.svg @@ -0,0 +1,4 @@ + + + + diff --git a/submodules/TelegramUI/Images.xcassets/Stories/SoundOff.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/SoundOff.imageset/Contents.json new file mode 100644 index 0000000000..cea4531ac1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/SoundOff.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "StorySoundOff2.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/SoundOff.imageset/StorySoundOff2.svg b/submodules/TelegramUI/Images.xcassets/Stories/SoundOff.imageset/StorySoundOff2.svg new file mode 100644 index 0000000000..92b39208d9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/SoundOff.imageset/StorySoundOff2.svg @@ -0,0 +1,3 @@ + + + diff --git a/submodules/TelegramUI/Images.xcassets/Stories/SoundOn.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/SoundOn.imageset/Contents.json new file mode 100644 index 0000000000..5751b3c0cd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/SoundOn.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "StorySoundOn.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/SoundOn.imageset/StorySoundOn.svg b/submodules/TelegramUI/Images.xcassets/Stories/SoundOn.imageset/StorySoundOn.svg new file mode 100644 index 0000000000..2997badb85 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/SoundOn.imageset/StorySoundOn.svg @@ -0,0 +1,3 @@ + + + diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index 8c0a716f9c..02239d3089 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -401,9 +401,9 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent func setSoundEnabled(_ value: Bool) { assert(Queue.mainQueue().isCurrent()) if value { - self.player.playOnceWithSound(playAndRecord: true) + self.player.playOnceWithSound(playAndRecord: true, seek: .none) } else { - self.player.continuePlayingWithoutSound() + self.player.continuePlayingWithoutSound(seek: .none) } } From 9c366b0155d42ea0352b9218e42fe6af1ef01264 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 27 Jun 2023 23:54:32 +0300 Subject: [PATCH 2/2] Merge more action --- .../StoryItemSetContainerComponent.swift | 190 +++--------------- 1 file changed, 24 insertions(+), 166 deletions(-) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 24d2dcbb25..0d30b1f100 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1756,167 +1756,7 @@ public final class StoryItemSetContainerComponent: Component { guard let self else { return } - - component.controller()?.forEachController { c in - if let c = c as? UndoOverlayController { - c.dismiss() - } - return true - } - - var items: [ContextMenuItem] = [] - - let additionalCount = component.slice.item.storyItem.privacy?.additionallyIncludePeers.count ?? 0 - - let privacyText: String - switch component.slice.item.storyItem.privacy?.base { - case .closeFriends: - privacyText = "Close Friends" - case .contacts: - if additionalCount != 0 { - privacyText = "Contacts (-\(additionalCount))" - } else { - privacyText = "Contacts" - } - case .nobody: - if additionalCount != 0 { - if additionalCount == 1 { - privacyText = "\(additionalCount) Person" - } else { - privacyText = "\(additionalCount) People" - } - } else { - privacyText = "Only Me" - } - default: - privacyText = "Everyone" - } - - items.append(.action(ContextMenuActionItem(text: "Who can see", textLayout: .secondLineWithValue(privacyText), icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - self.openItemPrivacySettings() - }))) - - items.append(.action(ContextMenuActionItem(text: "Edit Story", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - self.openStoryEditing() - }))) - - items.append(.separator) - - items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from profile" : "Save to profile", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Chat/Context Menu/Check" : "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return - } - - let _ = component.context.engine.messages.updateStoriesArePinned(ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).start() - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - if component.slice.item.storyItem.isPinned { - self.component?.presentController(UndoOverlayController( - presentationData: presentationData, - content: .info(title: nil, text: "Story removed from your profile", timeout: nil), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), nil) - } else { - self.component?.presentController(UndoOverlayController( - presentationData: presentationData, - content: .info(title: "Story saved to your profile", text: "Saved stories can be viewed by others on your profile until you remove them.", timeout: nil), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), nil) - } - }))) - - let saveText: String - if case .file = component.slice.item.storyItem.media { - saveText = "Save Video" - } else { - saveText = "Save Image" - } - items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - self.requestSave() - }))) - - if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) { - items.append(.action(ContextMenuActionItem(text: "Copy Link", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return - } - - let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id) - |> deliverOnMainQueue).start(next: { [weak self] link in - guard let self, let component = self.component else { - return - } - if let link { - UIPasteboard.general.string = link - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - component.presentController(UndoOverlayController( - presentationData: presentationData, - content: .linkCopied(text: "Link copied."), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), nil) - } - }) - }))) - items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - self.sendMessageContext.performShareAction(view: self) - }))) - } - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(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)) + self.performMoreAction(sourceView: sourceView, gesture: gesture) }, openPeer: { [weak self] peer in guard let self else { @@ -2108,7 +1948,7 @@ public final class StoryItemSetContainerComponent: Component { let tooltipScreen = TooltipScreen( account: component.context.account, sharedContext: component.context.sharedContext, - text: "This video has no sound", style: .default, location: TooltipScreen.Location.point(soundButtonView.convert(soundButtonView.bounds, to: self).offsetBy(dx: 1.0, dy: -10.0), .top), displayDuration: .manual(true), shouldDismissOnTouch: { _ in + text: .plain(text: "This video has no sound"), style: .default, location: TooltipScreen.Location.point(soundButtonView.convert(soundButtonView.bounds, to: self).offsetBy(dx: 1.0, dy: -10.0), .top), displayDuration: .manual(true), shouldDismissOnTouch: { _ in return .dismiss(consume: true) } ) @@ -2172,7 +2012,7 @@ public final class StoryItemSetContainerComponent: Component { let tooltipScreen = TooltipScreen( account: component.context.account, sharedContext: component.context.sharedContext, - text: "You are seeing this story because you have\nbeen added to \(component.slice.peer.compactDisplayTitle)'s list of close friends.", style: .default, location: TooltipScreen.Location.point(closeFriendIconView.convert(closeFriendIconView.bounds, to: self).offsetBy(dx: 1.0, dy: 6.0), .top), displayDuration: .manual(true), shouldDismissOnTouch: { _ in + text: .plain(text: "You are seeing this story because you have\nbeen added to \(component.slice.peer.compactDisplayTitle)'s list of close friends."), style: .default, location: TooltipScreen.Location.point(closeFriendIconView.convert(closeFriendIconView.bounds, to: self).offsetBy(dx: 1.0, dy: 6.0), .top), displayDuration: .manual(true), shouldDismissOnTouch: { _ in return .dismiss(consume: true) } ) @@ -3190,15 +3030,33 @@ public final class StoryItemSetContainerComponent: Component { }))) if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) { - items.append(.action(ContextMenuActionItem(text: "Copy link", icon: { theme in + items.append(.action(ContextMenuActionItem(text: "Copy Link", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) - guard let self else { + guard let self, let component = self.component else { return } - self.sendMessageContext.performCopyLinkAction(view: self) + + let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id) + |> deliverOnMainQueue).start(next: { [weak self] link in + guard let self, let component = self.component else { + return + } + if let link { + UIPasteboard.general.string = link + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + component.presentController(UndoOverlayController( + presentationData: presentationData, + content: .linkCopied(text: "Link copied."), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), nil) + } + }) }))) items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)