From 39b395c2fab5fdad070905f5a7777087b9a58bec Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 19 Jun 2023 23:59:38 +0300 Subject: [PATCH] Stories --- .../Sources/AccountContext.swift | 1 + .../Sources/ChatListController.swift | 4 +- .../Sources/ChatListControllerNode.swift | 2 + .../Sources/ContactsControllerNode.swift | 2 + .../Navigation/NavigationContainer.swift | 3 + .../Display/Source/ViewController.swift | 2 + .../Sources/TelegramBaseController.swift | 99 ++-- .../Sources/ChatListNavigationBar.swift | 45 +- .../MessageInputActionButtonComponent.swift | 2 +- .../Sources/PeerInfoStoryPaneNode.swift | 21 + .../Stories/StoryContainerScreen/BUILD | 5 + .../Sources/StoryContainerScreen.swift | 20 +- .../StoryContentCaptionComponent.swift | 205 +++++++- .../StoryItemSetContainerComponent.swift | 482 ++++++------------ ...StoryItemSetContainerViewSendMessage.swift | 260 ++++++++++ .../Sources/StoryChatContent.swift | 1 + .../Sources/StoryItemContentComponent.swift | 2 +- .../Sources/StoryPeerListComponent.swift | 3 +- .../Sources/StoryPeerListItemComponent.swift | 5 +- .../IconForwardSend.imageset/Contents.json | 12 + .../arrowshape_30.pdf | 167 ++++++ .../TelegramUI/Sources/OpenChatMessage.swift | 3 + .../Sources/SharedAccountContext.swift | 5 + .../WebpagePreviewAccessoryPanelNode.swift | 3 + 24 files changed, 975 insertions(+), 379 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Text/IconForwardSend.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Text/IconForwardSend.imageset/arrowshape_30.pdf diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 2898742a5b..12c58da368 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -867,6 +867,7 @@ public protocol SharedAccountContext: AnyObject { func makeStorageManagementController(context: AccountContext) -> ViewController func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? + func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String) -> ViewController func navigateToChatController(_ params: NavigateToChatControllerParams) func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController) func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 2c78eeda34..bb4dbbabe1 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -218,6 +218,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController super.init(context: context, navigationBarPresentationData: nil, mediaAccessoryPanelVisibility: .always, locationBroadcastPanelSource: .summary, groupCallPanelSource: groupCallPanelSource) + self.accessoryPanelContainer = ASDisplayNode() + self.tabBarItemContextActionType = .always self.automaticallyControlPresentationContextLayout = false @@ -2275,7 +2277,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController var primaryContent: ChatListHeaderComponent.Content? if let primaryContext = self.primaryContext { var backTitle: String? - if let previousItem = self.navigationBar?.previousItem { + if let previousItem = self.previousItem { switch previousItem { case let .item(item): backTitle = item.title ?? self.presentationData.strings.Common_Back diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 9930d5ccc4..7c2950ded0 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1884,6 +1884,8 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { uploadProgress: self.controller?.storyUploadProgress, tabsNode: tabsNode, tabsNodeIsSearch: tabsNodeIsSearch, + accessoryPanelContainer: self.controller?.accessoryPanelContainer, + accessoryPanelContainerHeight: self.controller?.accessoryPanelContainerHeight ?? 0.0, activateSearch: { [weak self] searchContentNode in guard let self, let controller = self.controller else { return diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index 0129bd72a6..7274455207 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -373,6 +373,8 @@ final class ContactsControllerNode: ASDisplayNode { uploadProgress: nil, tabsNode: tabsNode, tabsNodeIsSearch: tabsNodeIsSearch, + accessoryPanelContainer: nil, + accessoryPanelContainerHeight: 0.0, activateSearch: { [weak self] searchContentNode in guard let self else { return diff --git a/submodules/Display/Source/Navigation/NavigationContainer.swift b/submodules/Display/Source/Navigation/NavigationContainer.swift index e317248156..683547b170 100644 --- a/submodules/Display/Source/Navigation/NavigationContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationContainer.swift @@ -325,11 +325,14 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega if i == 0 { if canBeClosed { controllers[i].transitionNavigationBar?.previousItem = .close + controllers[i].previousItem = .close } else { controllers[i].transitionNavigationBar?.previousItem = nil + controllers[i].previousItem = nil } } else { controllers[i].transitionNavigationBar?.previousItem = .item(controllers[i - 1].navigationItem) + controllers[i].previousItem = .item(controllers[i - 1].navigationItem) } } diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index b7745a3820..5eaecf3ab4 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -155,6 +155,8 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { return self.prefersOnScreenNavigationHidden } + public internal(set) var previousItem: NavigationPreviousAction? + open var navigationPresentation: ViewControllerNavigationPresentation = .default open var _presentedInModal: Bool = false diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index ed3eeee0b1..2c89594ae0 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -61,6 +61,9 @@ private func presentLiveLocationController(context: AccountContext, peerId: Peer open class TelegramBaseController: ViewController, KeyShortcutResponder { private let context: AccountContext + public var accessoryPanelContainer: ASDisplayNode? + public private(set) var accessoryPanelContainerHeight: CGFloat = 0.0 + public let mediaAccessoryPanelVisibility: MediaAccessoryPanelVisibility public let locationBroadcastPanelSource: LocationBroadcastPanelSource public let groupCallPanelSource: GroupCallPanelSource @@ -76,7 +79,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { public var tempVoicePlaylistItemChanged: ((SharedMediaPlaylistItem?, SharedMediaPlaylistItem?) -> Void)? public var tempVoicePlaylistCurrentItem: SharedMediaPlaylistItem? - public var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? + public private(set) var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? private var locationBroadcastMode: LocationBroadcastNavigationAccessoryPanelMode? private var locationBroadcastPeers: [EnginePeer]? @@ -84,7 +87,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { private var locationBroadcastAccessoryPanel: LocationBroadcastNavigationAccessoryPanel? private var groupCallPanelData: GroupCallPanelData? - private var groupCallAccessoryPanel: GroupCallNavigationAccessoryPanel? + public private(set) var groupCallAccessoryPanel: GroupCallNavigationAccessoryPanel? private var dismissingPanel: ASDisplayNode? @@ -96,14 +99,16 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { override open var additionalNavigationBarHeight: CGFloat { var height: CGFloat = 0.0 - if let _ = self.groupCallAccessoryPanel { - height += 50.0 - } - if let _ = self.mediaAccessoryPanel { - height += MediaNavigationAccessoryHeaderNode.minimizedHeight - } - if let _ = self.locationBroadcastAccessoryPanel { - height += MediaNavigationAccessoryHeaderNode.minimizedHeight + if self.accessoryPanelContainer == nil { + if let _ = self.groupCallAccessoryPanel { + height += 50.0 + } + if let _ = self.mediaAccessoryPanel { + height += MediaNavigationAccessoryHeaderNode.minimizedHeight + } + if let _ = self.locationBroadcastAccessoryPanel { + height += MediaNavigationAccessoryHeaderNode.minimizedHeight + } } return height } @@ -401,16 +406,38 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { super.containerLayoutUpdated(layout, transition: transition) let navigationHeight = super.navigationLayout(layout: layout).navigationFrame.height - self.additionalNavigationBarHeight -// if !self.displayNavigationBar { -// navigationHeight = 0.0 -// } + + let mediaAccessoryPanelHidden: Bool + switch self.mediaAccessoryPanelVisibility { + case .always: + mediaAccessoryPanelHidden = false + case .none: + mediaAccessoryPanelHidden = true + case let .specific(size): + mediaAccessoryPanelHidden = size != layout.metrics.widthClass + } var additionalHeight: CGFloat = 0.0 + var panelStartY: CGFloat = 0.0 + if self.accessoryPanelContainer == nil { + var negativeHeight: CGFloat = 0.0 + if let _ = self.groupCallPanelData { + negativeHeight += 50.0 + } + if let _ = self.locationBroadcastPeers, let _ = self.locationBroadcastMode { + negativeHeight += MediaNavigationAccessoryHeaderNode.minimizedHeight + } + if let _ = self.playlistStateAndType, !mediaAccessoryPanelHidden { + negativeHeight += MediaNavigationAccessoryHeaderNode.minimizedHeight + } + panelStartY = navigationHeight.isZero ? (-negativeHeight) : (navigationHeight + additionalHeight + UIScreenPixel) + } if let groupCallPanelData = self.groupCallPanelData { let panelHeight: CGFloat = 50.0 - let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight.isZero ? -panelHeight : (navigationHeight + additionalHeight + UIScreenPixel)), size: CGSize(width: layout.size.width, height: panelHeight)) + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelStartY), size: CGSize(width: layout.size.width, height: panelHeight)) additionalHeight += panelHeight + panelStartY += panelHeight let groupCallAccessoryPanel: GroupCallNavigationAccessoryPanel if let current = self.groupCallAccessoryPanel { @@ -429,7 +456,11 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { activeCall: EngineGroupCallDescription(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title, scheduleTimestamp: groupCallPanelData.info.scheduleTimestamp, subscribedToScheduled: groupCallPanelData.info.subscribedToScheduled, isStream: groupCallPanelData.info.isStream) ) }) - self.navigationBar?.additionalContentNode.addSubnode(groupCallAccessoryPanel) + if let accessoryPanelContainer = self.accessoryPanelContainer { + accessoryPanelContainer.addSubnode(groupCallAccessoryPanel) + } else { + self.navigationBar?.additionalContentNode.addSubnode(groupCallAccessoryPanel) + } self.groupCallAccessoryPanel = groupCallAccessoryPanel groupCallAccessoryPanel.frame = panelFrame @@ -452,8 +483,9 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { if let locationBroadcastPeers = self.locationBroadcastPeers, let locationBroadcastMode = self.locationBroadcastMode { let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight - let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight.isZero ? -panelHeight : (navigationHeight + additionalHeight + UIScreenPixel)), size: CGSize(width: layout.size.width, height: panelHeight)) + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelStartY), size: CGSize(width: layout.size.width, height: panelHeight)) additionalHeight += panelHeight + panelStartY += panelHeight let locationBroadcastAccessoryPanel: LocationBroadcastNavigationAccessoryPanel if let current = self.locationBroadcastAccessoryPanel { @@ -576,7 +608,11 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } }) - self.navigationBar?.additionalContentNode.addSubnode(locationBroadcastAccessoryPanel) + if let accessoryPanelContainer = self.accessoryPanelContainer { + accessoryPanelContainer.addSubnode(locationBroadcastAccessoryPanel) + } else { + self.navigationBar?.additionalContentNode.addSubnode(locationBroadcastAccessoryPanel) + } self.locationBroadcastAccessoryPanel = locationBroadcastAccessoryPanel locationBroadcastAccessoryPanel.frame = panelFrame @@ -607,19 +643,12 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } } - let mediaAccessoryPanelHidden: Bool - switch self.mediaAccessoryPanelVisibility { - case .always: - mediaAccessoryPanelHidden = false - case .none: - mediaAccessoryPanelHidden = true - case let .specific(size): - mediaAccessoryPanelHidden = size != layout.metrics.widthClass - } - if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType, !mediaAccessoryPanelHidden { let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight - let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight.isZero ? -panelHeight : (navigationHeight + additionalHeight)), size: CGSize(width: layout.size.width, height: panelHeight)) + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelStartY), size: CGSize(width: layout.size.width, height: panelHeight)) + additionalHeight += panelHeight + panelStartY += panelHeight + if let (mediaAccessoryPanel, mediaType) = self.mediaAccessoryPanel, mediaType == type { transition.updateFrame(layer: mediaAccessoryPanel.layer, frame: panelFrame) mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, isHidden: !self.displayNavigationBar, transition: transition) @@ -848,9 +877,17 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } mediaAccessoryPanel.frame = panelFrame if let dismissingPanel = self.dismissingPanel { - self.navigationBar?.additionalContentNode.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel) + if let accessoryPanelContainer = self.accessoryPanelContainer { + accessoryPanelContainer.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel) + } else { + self.navigationBar?.additionalContentNode.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel) + } } else { - self.navigationBar?.additionalContentNode.addSubnode(mediaAccessoryPanel) + if let accessoryPanelContainer = self.accessoryPanelContainer { + accessoryPanelContainer.addSubnode(mediaAccessoryPanel) + } else { + self.navigationBar?.additionalContentNode.addSubnode(mediaAccessoryPanel) + } } self.mediaAccessoryPanel = (mediaAccessoryPanel, type) mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, isHidden: !self.displayNavigationBar, transition: .immediate) @@ -889,6 +926,8 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { self.suspendedNavigationBarLayout = suspendedNavigationBarLayout self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) } + + self.accessoryPanelContainerHeight = additionalHeight } open var keyShortcuts: [KeyShortcut] { diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index dd9d24e35f..18ea03cb0a 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -25,6 +25,8 @@ public final class ChatListNavigationBar: Component { public let uploadProgress: Float? public let tabsNode: ASDisplayNode? public let tabsNodeIsSearch: Bool + public let accessoryPanelContainer: ASDisplayNode? + public let accessoryPanelContainerHeight: CGFloat public let activateSearch: (NavigationBarSearchContentNode) -> Void public let openStatusSetup: (UIView) -> Void @@ -44,6 +46,8 @@ public final class ChatListNavigationBar: Component { uploadProgress: Float?, tabsNode: ASDisplayNode?, tabsNodeIsSearch: Bool, + accessoryPanelContainer: ASDisplayNode?, + accessoryPanelContainerHeight: CGFloat, activateSearch: @escaping (NavigationBarSearchContentNode) -> Void, openStatusSetup: @escaping (UIView) -> Void ) { @@ -62,6 +66,8 @@ public final class ChatListNavigationBar: Component { self.uploadProgress = uploadProgress self.tabsNode = tabsNode self.tabsNodeIsSearch = tabsNodeIsSearch + self.accessoryPanelContainer = accessoryPanelContainer + self.accessoryPanelContainerHeight = accessoryPanelContainerHeight self.activateSearch = activateSearch self.openStatusSetup = openStatusSetup } @@ -112,6 +118,12 @@ public final class ChatListNavigationBar: Component { if lhs.tabsNodeIsSearch != rhs.tabsNodeIsSearch { return false } + if lhs.accessoryPanelContainer !== rhs.accessoryPanelContainer { + return false + } + if lhs.accessoryPanelContainerHeight != rhs.accessoryPanelContainerHeight { + return false + } return true } @@ -199,7 +211,7 @@ public final class ChatListNavigationBar: Component { } public func applyScroll(offset: CGFloat, allowAvatarsExpansion: Bool, forceUpdate: Bool = false, transition: Transition) { - if self.currentAllowAvatarsExpansion != allowAvatarsExpansion, allowAvatarsExpansion { + if self.currentAllowAvatarsExpansion != allowAvatarsExpansion, allowAvatarsExpansion, !transition.animation.isImmediate { self.addStoriesUnlockedAnimation(duration: 0.3, animateScrollUnlocked: false) } @@ -317,6 +329,9 @@ public final class ChatListNavigationBar: Component { if component.tabsNode != nil { searchFrame.origin.y -= 40.0 } + if !component.isSearchActive { + searchFrame.origin.y -= component.accessoryPanelContainerHeight + } let clippedSearchOffset = max(0.0, min(clippedScrollOffset - effectiveStoriesOffsetDistance, searchOffsetDistance)) let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance @@ -407,10 +422,15 @@ public final class ChatListNavigationBar: Component { } var tabsFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: 46.0)) + if !component.isSearchActive { + tabsFrame.origin.y -= component.accessoryPanelContainerHeight + } if component.tabsNode != nil { tabsFrame.origin.y -= 46.0 } + let accessoryPanelContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height - component.accessoryPanelContainerHeight), size: CGSize(width: visibleSize.width, height: component.accessoryPanelContainerHeight)) + if let disappearingTabsView = self.disappearingTabsView { disappearingTabsView.layer.anchorPoint = CGPoint() transition.setFrameWithAdditivePosition(view: disappearingTabsView, frame: tabsFrame.offsetBy(dx: 0.0, dy: self.disappearingTabsViewSearch ? (-currentLayout.size.height + 2.0) : 0.0)) @@ -441,6 +461,23 @@ public final class ChatListNavigationBar: Component { tabsNodeTransition.setFrameWithAdditivePosition(view: tabsNode.view, frame: tabsFrame.offsetBy(dx: 0.0, dy: component.tabsNodeIsSearch ? (-currentLayout.size.height + 2.0) : 0.0)) } + + if let accessoryPanelContainer = component.accessoryPanelContainer { + var tabsNodeTransition = transition + if accessoryPanelContainer.view.superview !== self { + accessoryPanelContainer.view.layer.anchorPoint = CGPoint() + tabsNodeTransition = .immediate + accessoryPanelContainer.view.alpha = 1.0 + self.addSubview(accessoryPanelContainer.view) + if !transition.animation.isImmediate { + accessoryPanelContainer.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } else { + transition.setAlpha(view: accessoryPanelContainer.view, alpha: 1.0) + } + + tabsNodeTransition.setFrameWithAdditivePosition(view: accessoryPanelContainer.view, frame: accessoryPanelContainerFrame) + } } public func updateStoryUploadProgress(storyUploadProgress: Float?) { @@ -464,6 +501,8 @@ public final class ChatListNavigationBar: Component { uploadProgress: storyUploadProgress, tabsNode: component.tabsNode, tabsNodeIsSearch: component.tabsNodeIsSearch, + accessoryPanelContainer: component.accessoryPanelContainer, + accessoryPanelContainerHeight: component.accessoryPanelContainerHeight, activateSearch: component.activateSearch, openStatusSetup: component.openStatusSetup ) @@ -547,6 +586,10 @@ public final class ChatListNavigationBar: Component { contentHeight += 40.0 } + if component.accessoryPanelContainer != nil && !component.isSearchActive { + contentHeight += component.accessoryPanelContainerHeight + } + let size = CGSize(width: availableSize.width, height: contentHeight) self.currentLayout = CurrentLayout(size: size) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift index 2cd2bd451d..0c7df6c5db 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift @@ -16,7 +16,7 @@ private extension MessageInputActionButtonComponent.Mode { case .attach: return "Chat/Input/Text/IconAttachment" case .forward: - return "Chat/Input/Text/IconForward" + return "Chat/Input/Text/IconForwardSend" default: return nil } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 11faf768e3..f7f6894b5d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -889,6 +889,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + + private weak var pendingOpenListContext: PeerStoryListContentContextImpl? public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?, listContext: PeerStoryListContext?) { self.context = context @@ -952,6 +954,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } } + if self.pendingOpenListContext != nil { + return + } + //TODO:selection let listContext = PeerStoryListContentContextImpl( context: self.context, @@ -959,6 +965,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr listContext: self.listSource, initialId: item.story.id ) + self.pendingOpenListContext = listContext + self.itemGrid.isUserInteractionEnabled = false + let _ = (listContext.state |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in @@ -966,6 +975,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return } + guard let pendingOpenListContext = self.pendingOpenListContext, pendingOpenListContext === listContext else { + return + } + self.pendingOpenListContext = nil + self.itemGrid.isUserInteractionEnabled = true + var transitionIn: StoryContainerScreen.TransitionIn? let story = item.story @@ -1016,6 +1031,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr }, updateView: { view, state, transition in (view as? ItemTransitionView)?.update(state: state, transition: transition) + }, + insertCloneTransitionView: { [weak self] view in + guard let self else { + return + } + self.view.insertSubview(view, aboveSubview: self.itemGrid.view) } ), destinationRect: self.itemGrid.view.convert(itemRect, to: self.view), diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 928b85104d..1ed9e6218d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -56,6 +56,11 @@ swift_library( "//submodules/TelegramStringFormatting", "//submodules/ShimmerEffect", "//submodules/ImageCompression", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/InvisibleInkDustNode", + "//submodules/PresentationDataUtils", + "//submodules/UrlEscaping", + "//submodules/OverlayStatusController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index ade16dd8de..6680a14df7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -851,7 +851,7 @@ private final class StoryContainerScreenComponent: Component { } let itemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - itemSetContainerSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - itemSetContainerSize.height) / 2.0)), size: itemSetContainerSize) - if let itemSetComponentView = itemSetView.view.view { + if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { if itemSetView.superview == nil { self.addSubview(itemSetView) } @@ -866,6 +866,10 @@ private final class StoryContainerScreenComponent: Component { itemSetTransition.setBounds(view: itemSetView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) itemSetTransition.setSublayerTransform(view: itemSetView, transform: CATransform3DMakeScale(dismissPanScale, dismissPanScale, 1.0)) + itemSetTransition.setPosition(view: itemSetComponentView.transitionCloneContainerView, position: itemFrame.center.offsetBy(dx: 0.0, dy: dismissPanOffset)) + itemSetTransition.setBounds(view: itemSetComponentView.transitionCloneContainerView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + itemSetTransition.setSublayerTransform(view: itemSetComponentView.transitionCloneContainerView, transform: CATransform3DMakeScale(dismissPanScale, dismissPanScale, 1.0)) + itemSetTransition.setPosition(view: itemSetComponentView, position: CGRect(origin: CGPoint(), size: itemFrame.size).center) itemSetTransition.setBounds(view: itemSetComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) @@ -1037,13 +1041,16 @@ public class StoryContainerScreen: ViewControllerComponentContainer { public final class TransitionView { public let makeView: () -> UIView public let updateView: (UIView, TransitionState, Transition) -> Void + public let insertCloneTransitionView: ((UIView) -> Void)? public init( makeView: @escaping () -> UIView, - updateView: @escaping (UIView, TransitionState, Transition) -> Void + updateView: @escaping (UIView, TransitionState, Transition) -> Void, + insertCloneTransitionView: ((UIView) -> Void)? ) { self.makeView = makeView self.updateView = updateView + self.insertCloneTransitionView = insertCloneTransitionView } } @@ -1092,6 +1099,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer { } private let context: AccountContext + private var didAnimateIn: Bool = false private var isDismissed: Bool = false private let focusedItemPromise = Promise(nil) @@ -1141,8 +1149,12 @@ public class StoryContainerScreen: ViewControllerComponentContainer { self.view.disablesInteractiveModalDismiss = true - if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View { - componentView.animateIn() + if !self.didAnimateIn { + self.didAnimateIn = true + + if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View { + componentView.animateIn() + } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index b72b2123e1..082c3c9d6c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -3,8 +3,23 @@ import UIKit import Display import ComponentFlow import MultilineTextComponent +import AccountContext +import TelegramCore +import TextNodeWithEntities +import TextFormat +import InvisibleInkDustNode +import UrlEscaping final class StoryContentCaptionComponent: Component { + enum Action { + case url(url: String, concealed: Bool) + case textMention(String) + case peerMention(peerId: EnginePeer.Id, mention: String) + case hashtag(String?, String) + case bankCard(String) + case customEmoji(TelegramMediaFile) + } + final class ExternalState { fileprivate(set) var isExpanded: Bool = false @@ -25,20 +40,38 @@ final class StoryContentCaptionComponent: Component { } let externalState: ExternalState + let context: AccountContext let text: String + let entities: [MessageTextEntity] + let action: (Action) -> Void init( externalState: ExternalState, - text: String + context: AccountContext, + text: String, + entities: [MessageTextEntity], + action: @escaping (Action) -> Void ) { self.externalState = externalState + self.context = context self.text = text + self.entities = entities + self.action = action } static func ==(lhs: StoryContentCaptionComponent, rhs: StoryContentCaptionComponent) -> Bool { + if lhs.externalState !== rhs.externalState { + return false + } + if lhs.context !== rhs.context { + return false + } if lhs.text != rhs.text { return false } + if lhs.entities != rhs.entities { + return false + } return true } @@ -70,7 +103,9 @@ final class StoryContentCaptionComponent: Component { private let shadowGradientLayer: SimpleGradientLayer private let shadowPlainLayer: SimpleLayer - private let text = ComponentView() + private var textNode: TextNodeWithEntities? + private var linkHighlightingNode: LinkHighlightingNode? + private var dustNode: InvisibleInkDustNode? private var component: StoryContentCaptionComponent? private weak var state: EmptyComponentState? @@ -133,10 +168,10 @@ final class StoryContentCaptionComponent: Component { if !self.bounds.contains(point) { return nil } - if let textView = self.text.view { + if let textView = self.textNode?.textNode.view { let textLocalPoint = self.convert(point, to: textView) if textLocalPoint.y >= -7.0 { - return self.scrollView + return textView } } @@ -205,6 +240,103 @@ final class StoryContentCaptionComponent: Component { } } + @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let component = self.component, let textNode = self.textNode { + switch gesture { + case .tap: + let titleFrame = textNode.textNode.view.bounds + if titleFrame.contains(location) { + if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) { + return + } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + var concealed = true + if let (attributeText, fullText) = textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) + } + component.action(.url(url: url, concealed: concealed)) + return + } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { + component.action(.peerMention(peerId: peerMention.peerId, mention: peerMention.mention)) + return + } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + component.action(.textMention(peerName)) + return + } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { + component.action(.hashtag(hashtag.peerName, hashtag.hashtag)) + return + } else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String { + component.action(.bankCard(bankCard)) + return + } else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file { + component.action(.customEmoji(file)) + return + } + } + } + + self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + default: + break + } + } + default: + break + } + } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + guard let textNode = self.textNode else { + return + } + var rects: [CGRect]? + var spoilerRects: [CGRect]? + if let point = point { + let textNodeFrame = textNode.textNode.bounds + if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let possibleNames: [String] = [ + TelegramTextAttributes.URL, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag, + TelegramTextAttributes.Timecode, + TelegramTextAttributes.BankCard + ] + for name in possibleNames { + if let _ = attributes[NSAttributedString.Key(rawValue: name)] { + rects = textNode.textNode.attributeRects(name: name, at: index) + break + } + } + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)] { + spoilerRects = textNode.textNode.attributeRects(name: TelegramTextAttributes.Spoiler, at: index) + } + } + } + + if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, let dustNode = self.dustNode, !dustNode.isRevealed { + } else if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: UIColor(white: 1.0, alpha: 0.5)) + self.linkHighlightingNode = linkHighlightingNode + self.scrollView.insertSubview(linkHighlightingNode.view, belowSubview: textNode.textNode.view) + } + linkHighlightingNode.frame = textNode.textNode.view.frame + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + func update(component: StoryContentCaptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.ignoreExternalState = true @@ -213,29 +345,64 @@ final class StoryContentCaptionComponent: Component { let sideInset: CGFloat = 16.0 let verticalInset: CGFloat = 7.0 - let textContainerSize = CGSize(width: availableSize.width - sideInset * 2.0 - 50.0, height: availableSize.height - verticalInset * 2.0) + let textContainerSize = CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - verticalInset * 2.0) - let textSize = self.text.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.text, font: Font.regular(16.0), textColor: .white)), - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: textContainerSize + let attributedText = stringWithAppliedEntities( + component.text, + entities: component.entities, + baseColor: .white, + linkColor: .white, + baseFont: Font.regular(16.0), + linkFont: Font.regular(16.0), + boldFont: Font.semibold(16.0), + italicFont: Font.italic(16.0), + boldItalicFont: Font.semiboldItalic(16.0), + fixedFont: Font.monospace(16.0), + blockQuoteFont: Font.monospace(16.0), + message: nil ) + let makeLayout = TextNodeWithEntities.asyncLayout(self.textNode) + let textLayout = makeLayout(TextNodeLayoutArguments( + attributedString: attributedText, + maximumNumberOfLines: 0, + truncationType: .end, + constrainedSize: textContainerSize + )) + let maxHeight: CGFloat = 50.0 - let visibleTextHeight = min(maxHeight, textSize.height) - let textOverflowHeight: CGFloat = textSize.height - visibleTextHeight + let visibleTextHeight = min(maxHeight, textLayout.0.size.height) + let textOverflowHeight: CGFloat = textLayout.0.size.height - visibleTextHeight let scrollContentSize = CGSize(width: availableSize.width, height: availableSize.height + textOverflowHeight) - if let textView = self.text.view { - if textView.superview == nil { - self.scrollView.addSubview(textView) + let textNode = textLayout.1(TextNodeWithEntities.Arguments( + context: component.context, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + placeholderColor: UIColor(white: 0.2, alpha: 1.0), attemptSynchronous: true + )) + if self.textNode !== textNode { + self.textNode?.textNode.view.removeFromSuperview() + + self.textNode = textNode + if textNode.textNode.view.superview == nil { + self.scrollView.addSubview(textNode.textNode.view) + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { point in + return .waitForSingleTap + } + recognizer.highlight = { [weak self] point in + guard let self else { + return + } + self.updateTouchesAtPoint(point) + } + textNode.textNode.view.addGestureRecognizer(recognizer) } - textView.frame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: textSize) } + + textNode.textNode.frame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: textLayout.0.size) self.itemLayout = ItemLayout( containerSize: availableSize, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 38752ed02a..8a8198194c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -6,7 +6,6 @@ import AppBundle import ComponentDisplayAdapters import ReactionSelectionNode import EntityKeyboard -import StoryFooterPanelComponent import MessageInputPanelComponent import TelegramPresentationData import SwiftSignalKit @@ -22,6 +21,7 @@ import ImageCompression import ShareWithPeersScreen import PlainButtonComponent import TooltipUI +import PresentationDataUtils public final class StoryItemSetContainerComponent: Component { public final class ExternalState { @@ -242,7 +242,6 @@ public final class StoryItemSetContainerComponent: Component { var captionItem: CaptionItem? let inputPanel = ComponentView() - let footerPanel = ComponentView() let inputPanelExternalState = MessageInputPanelComponent.ExternalState() var displayViewList: Bool = false @@ -274,6 +273,8 @@ public final class StoryItemSetContainerComponent: Component { private weak var voiceMessagesRestrictedTooltipController: TooltipController? + let transitionCloneContainerView: UIView + override init(frame: CGRect) { self.sendMessageContext = StoryItemSetContainerSendMessage() @@ -294,6 +295,8 @@ public final class StoryItemSetContainerComponent: Component { self.closeButton = HighlightableButton() self.closeButtonIconView = UIImageView() + self.transitionCloneContainerView = UIView() + super.init(frame: frame) self.scrollView.delaysContentTouches = false @@ -648,8 +651,10 @@ public final class StoryItemSetContainerComponent: Component { return } if component.slice.peer.id == component.context.account.peerId { - self.displayViewList = true - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + if let views = component.slice.item.storyItem.views, !views.seenPeers.isEmpty { + self.displayViewList = true + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } } else { if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View { inputPanelView.activateInput() @@ -670,16 +675,6 @@ public final class StoryItemSetContainerComponent: Component { ) inputPanelView.layer.animateAlpha(from: 0.0, to: inputPanelView.alpha, duration: 0.28) } - if let footerPanelView = self.footerPanel.view { - footerPanelView.layer.animatePosition( - from: CGPoint(x: 0.0, y: self.bounds.height - footerPanelView.frame.minY), - to: CGPoint(), - duration: 0.3, - timingFunction: kCAMediaTimingFunctionSpring, - additive: true - ) - footerPanelView.layer.animateAlpha(from: 0.0, to: footerPanelView.alpha, duration: 0.28) - } if let viewListView = self.viewList?.view.view { viewListView.layer.animatePosition( from: CGPoint(x: 0.0, y: self.bounds.height - self.contentContainerView.frame.maxY), @@ -749,9 +744,7 @@ public final class StoryItemSetContainerComponent: Component { } func animateOut(transitionOut: StoryContainerScreen.TransitionOut, completion: @escaping () -> Void) { - self.closeButton.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - completion() - }) + var cleanups: [() -> Void] = [] if let inputPanelView = self.inputPanel.view { inputPanelView.layer.animatePosition( @@ -764,17 +757,6 @@ public final class StoryItemSetContainerComponent: Component { ) inputPanelView.layer.animateAlpha(from: inputPanelView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false) } - if let footerPanelView = self.footerPanel.view { - footerPanelView.layer.animatePosition( - from: CGPoint(), - to: CGPoint(x: 0.0, y: self.bounds.height - footerPanelView.frame.minY), - duration: 0.3, - timingFunction: kCAMediaTimingFunctionSpring, - removeOnCompletion: false, - additive: true - ) - footerPanelView.layer.animateAlpha(from: footerPanelView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false) - } if let viewListView = self.viewList?.view.view { viewListView.layer.animatePosition( from: CGPoint(), @@ -811,39 +793,75 @@ public final class StoryItemSetContainerComponent: Component { if let rightInfoView = self.rightInfoItem?.view.view { if transitionOut.destinationIsAvatar { let transitionView = transitionOut.transitionView - let transitionViewImpl = transitionView?.makeView() - if let transitionViewImpl { - self.insertSubview(transitionViewImpl, aboveSubview: self.contentContainerView) + + var transitionViewsImpl: [UIView] = [] + + if let transitionViewImpl = transitionView?.makeView() { + transitionViewsImpl.append(transitionViewImpl) + + let transitionSourceContainerView = UIView(frame: self.bounds) + transitionSourceContainerView.isUserInteractionEnabled = false + self.insertSubview(transitionSourceContainerView, aboveSubview: self.contentContainerView) + + transitionSourceContainerView.addSubview(transitionViewImpl) + + if let insertCloneTransitionView = transitionView?.insertCloneTransitionView { + if let transitionCloneViewImpl = transitionView?.makeView() { + transitionViewsImpl.append(transitionCloneViewImpl) + + let transitionCloneContainerView = self.transitionCloneContainerView + transitionCloneContainerView.isUserInteractionEnabled = false + insertCloneTransitionView(transitionCloneContainerView) + transitionCloneContainerView.frame = transitionCloneContainerView.convert(self.convert(self.bounds, to: nil), from: nil) + + transitionCloneContainerView.addSubview(transitionCloneViewImpl) + + transitionSourceContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, removeOnCompletion: false) + transitionCloneContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + + cleanups.append({ [weak transitionCloneContainerView] in + transitionCloneContainerView?.removeFromSuperview() + }) + } + } let rightInfoSourceFrame = rightInfoView.convert(rightInfoView.bounds, to: self) let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: sourceLocalFrame.center, to: rightInfoSourceFrame.center, elevation: 0.0, duration: 0.3, curve: .spring, reverse: true) - transitionViewImpl.frame = rightInfoSourceFrame - transitionViewImpl.alpha = 0.0 - transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( - sourceSize: rightInfoSourceFrame.size, - destinationSize: sourceLocalFrame.size, - progress: 0.0 - ), .immediate) + for transitionViewImpl in transitionViewsImpl { + transitionViewImpl.frame = rightInfoSourceFrame + transitionViewImpl.alpha = 0.0 + transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( + sourceSize: rightInfoSourceFrame.size, + destinationSize: sourceLocalFrame.size, + progress: 0.0 + ), .immediate) + } let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) - transitionViewImpl.alpha = 1.0 - transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + for transitionViewImpl in transitionViewsImpl { + transitionViewImpl.alpha = 1.0 + 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) - transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) + for transitionViewImpl in transitionViewsImpl { + transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) + } - transitionViewImpl.layer.position = positionKeyframes[positionKeyframes.count - 1] - transitionViewImpl.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", removeOnCompletion: false, additive: false) - transitionViewImpl.layer.animateBounds(from: CGRect(origin: CGPoint(), size: rightInfoSourceFrame.size), to: CGRect(origin: CGPoint(), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - - transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( - sourceSize: rightInfoSourceFrame.size, - destinationSize: sourceLocalFrame.size, - progress: 1.0 - ), transition) + for transitionViewImpl in transitionViewsImpl { + transitionViewImpl.layer.position = positionKeyframes[positionKeyframes.count - 1] + transitionViewImpl.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", removeOnCompletion: false, additive: false) + transitionViewImpl.layer.animateBounds(from: CGRect(origin: CGPoint(), size: rightInfoSourceFrame.size), to: CGRect(origin: CGPoint(), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + + transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( + sourceSize: rightInfoSourceFrame.size, + destinationSize: sourceLocalFrame.size, + progress: 1.0 + ), transition) + } } let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: innerSourceLocalFrame.center, to: rightInfoView.layer.position, elevation: 0.0, duration: 0.3, curve: .spring, reverse: true) @@ -867,30 +885,63 @@ public final class StoryItemSetContainerComponent: Component { if !transitionOut.destinationIsAvatar { let transitionView = transitionOut.transitionView - let transitionViewImpl = transitionView?.makeView() - if let transitionViewImpl { - self.insertSubview(transitionViewImpl, belowSubview: self.contentContainerView) + + var transitionViewsImpl: [UIView] = [] + + if let transitionViewImpl = transitionView?.makeView() { + transitionViewsImpl.append(transitionViewImpl) - transitionViewImpl.frame = contentSourceFrame - transitionViewImpl.alpha = 0.0 - transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( - sourceSize: contentSourceFrame.size, - destinationSize: sourceLocalFrame.size, - progress: 0.0 - ), .immediate) + let transitionSourceContainerView = UIView(frame: self.bounds) + transitionSourceContainerView.isUserInteractionEnabled = false + self.insertSubview(transitionSourceContainerView, belowSubview: self.contentContainerView) + + transitionSourceContainerView.addSubview(transitionViewImpl) + + if let insertCloneTransitionView = transitionView?.insertCloneTransitionView { + if let transitionCloneViewImpl = transitionView?.makeView() { + transitionViewsImpl.append(transitionCloneViewImpl) + + let transitionCloneContainerView = self.transitionCloneContainerView + transitionCloneContainerView.isUserInteractionEnabled = false + insertCloneTransitionView(transitionCloneContainerView) + + transitionCloneContainerView.addSubview(transitionCloneViewImpl) + + transitionSourceContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, removeOnCompletion: false) + transitionCloneContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + + cleanups.append({ [weak transitionCloneContainerView] in + transitionCloneContainerView?.removeFromSuperview() + }) + } + } + + for transitionViewImpl in transitionViewsImpl { + transitionViewImpl.frame = contentSourceFrame + transitionViewImpl.alpha = 0.0 + transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( + sourceSize: contentSourceFrame.size, + destinationSize: sourceLocalFrame.size, + progress: 0.0 + ), .immediate) + } let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) - transitionViewImpl.alpha = 1.0 - transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + for transitionViewImpl in transitionViewsImpl { + transitionViewImpl.alpha = 1.0 + transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } self.contentContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) - transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( - sourceSize: contentSourceFrame.size, - destinationSize: sourceLocalFrame.size, - progress: 1.0 - ), transition) + for transitionViewImpl in transitionViewsImpl { + transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) + transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState( + sourceSize: contentSourceFrame.size, + destinationSize: sourceLocalFrame.size, + progress: 1.0 + ), transition) + } } } @@ -919,6 +970,14 @@ public final class StoryItemSetContainerComponent: Component { visibleItemView.layer.animateScale(from: 1.0, to: innerScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } } + + self.closeButton.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + for cleanup in cleanups { + cleanup() + } + cleanups.removeAll() + completion() + }) } func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -1112,238 +1171,6 @@ public final class StoryItemSetContainerComponent: Component { containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) ) - /*let footerPanelSize = self.footerPanel.update( - transition: transition, - component: AnyComponent(StoryFooterPanelComponent( - context: component.context, - storyItem: currentItem?.storyItem, - expandViewStats: { [weak self] in - guard let self else { - return - } - - if !self.displayViewList { - self.displayViewList = true - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) - } - }, - deleteAction: { [weak self] in - guard let self, let component = self.component else { - return - } - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - let actionSheet = ActionSheetController(presentationData: presentationData) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Delete", color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let self, let component = self.component else { - 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: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - - actionSheet.dismissed = { [weak self] _ in - guard let self else { - return - } - self.actionSheet = nil - self.updateIsProgressPaused() - } - self.actionSheet = actionSheet - self.updateIsProgressPaused() - - component.presentController(actionSheet) - }, - moreAction: { [weak self] sourceView, gesture in - guard let self, let component = self.component, let controller = component.controller() else { - return - } - - 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: - if additionalCount != 0 { - privacyText = "Close Friends (+\(additionalCount)" - } else { - 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) - - component.controller()?.forEachController { c in - if let c = c as? UndoOverlayController { - c.dismiss() - } - return true - } - - 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() - - if component.slice.item.storyItem.isPinned { - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - 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 } - )) - } else { - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - 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 } - )) - } - }))) - items.append(.action(ContextMenuActionItem(text: "Save image", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) - }, action: { _, a in - a(.default) - }))) - - if component.slice.item.storyItem.isPublic { - 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 } - )) - } - }) - }))) - items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) - }, action: { _, a in - a(.default) - }))) - } - - 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)) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 200.0) - )*/ - let bottomContentInsetWithoutInput = bottomContentInset var viewListInset: CGFloat = 0.0 @@ -1379,8 +1206,10 @@ public final class StoryItemSetContainerComponent: Component { let outerExpansionFraction: CGFloat if self.displayViewList { outerExpansionFraction = 1.0 - } else { + } else if let views = component.slice.item.storyItem.views, !views.seenPeers.isEmpty { outerExpansionFraction = component.verticalPanFraction + } else { + outerExpansionFraction = 0.0 } viewList.view.parentState = state @@ -1762,7 +1591,7 @@ public final class StoryItemSetContainerComponent: Component { if let view = currentCenterInfoItem.view.view { var animateIn = false if view.superview == nil { - self.contentContainerView.addSubview(view) + self.contentContainerView.insertSubview(view, belowSubview: self.closeButton) animateIn = true } transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: 10.0), size: centerInfoItemSize)) @@ -1857,7 +1686,38 @@ public final class StoryItemSetContainerComponent: Component { transition: captionItemTransition, component: AnyComponent(StoryContentCaptionComponent( externalState: captionItem.externalState, - text: component.slice.item.storyItem.text + context: component.context, + text: component.slice.item.storyItem.text, + entities: component.slice.item.storyItem.entities, + action: { [weak self] action in + guard let self, let component = self.component else { + return + } + switch action { + case let .url(url, concealed): + openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in + guard let self, let component = self.component, let controller = component.controller() else { + return + } + controller.present(c, in: .window(.root)) + }, openResolved: { [weak self] resolved in + guard let self else { + return + } + self.sendMessageContext.openResolved(view: self, result: resolved, forceExternal: false, concealed: concealed) + }) + case let .textMention(value): + self.sendMessageContext.openPeerMention(view: self, name: value) + case let .peerMention(peerId, _): + self.sendMessageContext.openPeerMention(view: self, peerId: peerId) + case let .hashtag(username, value): + self.sendMessageContext.openHashtag(view: self, hashtag: value, peerName: username) + case let .bankCard(value): + let _ = value + case .customEmoji: + break + } + } )), environment: {}, containerSize: CGSize(width: availableSize.width, height: contentFrame.height) @@ -2109,22 +1969,6 @@ public final class StoryItemSetContainerComponent: Component { } } - /*var footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize) - var footerPanelAlpha: CGFloat = (focusedItem?.isMy == true && !self.displayViewList) ? 1.0 : 0.0 - if case .regular = component.metrics.widthClass { - footerPanelAlpha *= component.visibilityFraction - } - if self.displayViewList { - footerPanelFrame.origin.y += footerPanelSize.height - } - if let footerPanelView = self.footerPanel.view { - if footerPanelView.superview == nil { - self.addSubview(footerPanelView) - } - transition.setFrame(view: footerPanelView, frame: footerPanelFrame) - transition.setAlpha(view: footerPanelView, alpha: footerPanelAlpha) - }*/ - let bottomGradientHeight = inputPanelSize.height + 32.0 transition.setFrame(layer: self.bottomContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: availableSize.height - component.inputHeight - bottomGradientHeight), size: CGSize(width: contentFrame.width, height: bottomGradientHeight))) //transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 04039aa915..4e8e8fab19 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -32,6 +32,8 @@ import TelegramPresentationData import ShareController import ChatPresentationInterfaceState import Postbox +import OverlayStatusController +import PresentationDataUtils final class StoryItemSetContainerSendMessage { weak var attachmentController: AttachmentController? @@ -46,6 +48,8 @@ final class StoryItemSetContainerSendMessage { var videoRecorder = Promise() let controllerNavigationDisposable = MetaDisposable() let enqueueMediaMessageDisposable = MetaDisposable() + let navigationActionDisposable = MetaDisposable() + let resolvePeerByNameDisposable = MetaDisposable() private(set) var isMediaRecordingLocked: Bool = false var wasRecordingDismissed: Bool = false @@ -53,6 +57,8 @@ final class StoryItemSetContainerSendMessage { deinit { self.controllerNavigationDisposable.dispose() self.enqueueMediaMessageDisposable.dispose() + self.navigationActionDisposable.dispose() + self.resolvePeerByNameDisposable.dispose() } func performSendMessageAction( @@ -1797,4 +1803,258 @@ final class StoryItemSetContainerSendMessage { let _ = (legacyAssetPickerEnqueueMessages(context: component.context, account: component.context.account, signals: signals) |> deliverOnMainQueue).start() } + + func openResolved(view: StoryItemSetContainerComponent.View, result: ResolvedUrl, forceExternal: Bool = false, concealed: Bool = false) { + guard let component = view.component, let navigationController = component.controller()?.navigationController as? NavigationController else { + return + } + let peerId = component.slice.peer.id + component.context.sharedContext.openResolvedUrl(result, context: component.context, urlContext: .chat(peerId: peerId, updatedPresentationData: nil), navigationController: navigationController, forceExternal: forceExternal, openPeer: { [weak self, weak view] peerId, navigation in + guard let self, let view, let component = view.component, let controller = component.controller() as? StoryContainerScreen else { + return + } + + controller.dismissWithoutTransitionOut() + + switch navigation { + case let .chat(_, subject, peekData): + if let navigationController = controller.navigationController as? NavigationController { + if case let .channel(channel) = peerId, channel.flags.contains(.isForum) { + component.context.sharedContext.navigateToForumChannel(context: component.context, peerId: peerId.id, navigationController: navigationController) + } else { + component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peerId), subject: subject, keepStack: .always, peekData: peekData, 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) + })) + } + } + case .info: + self.navigationActionDisposable.set((component.context.account.postbox.loadedPeerWithId(peerId.id) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak view] peer in + guard let view, let component = view.component else { + return + } + if peer.restrictionText(platform: "ios", contentSettings: component.context.currentContentSettings.with { $0 }) == nil { + if let infoController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + component.controller()?.push(infoController) + } + } + })) + case let .withBotStartPayload(startPayload): + if let navigationController = controller.navigationController as? NavigationController { + component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peerId), botStart: startPayload, keepStack: .always)) + } + case let .withAttachBot(attachBotStart): + if let navigationController = controller.navigationController as? NavigationController { + component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peerId), attachBotStart: attachBotStart)) + } + default: + break + } + }, + sendFile: nil, + sendSticker: nil, + requestMessageActionUrlAuth: nil, + joinVoiceChat: nil, + present: { [weak view] c, a in + guard let view, let component = view.component, let controller = component.controller() else { + return + } + controller.present(c, in: .window(.root), with: a) + }, dismissInput: { [weak view] in + guard let view else { + return + } + view.endEditing(true) + }, + contentContext: nil + ) + } + + func navigateToMessage(view: StoryItemSetContainerComponent.View, messageId: EngineMessage.Id, completion: (() -> Void)?) { + guard let component = view.component else { + return + } + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)) + |> deliverOnMainQueue).start(next: { [weak view] peer in + guard let view, let component = view.component, let controller = component.controller(), let peer = peer else { + return + } + if let navigationController = controller.navigationController as? NavigationController { + component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: true, timecode: nil))) + } + completion?() + }) + } + + func openPeerMention(view: StoryItemSetContainerComponent.View, name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil) { + guard let component = view.component, let parentController = component.controller() else { + return + } + let disposable = self.resolvePeerByNameDisposable + var resolveSignal = component.context.engine.peers.resolvePeerByName(name: name, ageLimit: 10) + + var cancelImpl: (() -> Void)? + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { [weak parentController] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + parentController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + resolveSignal = resolveSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { [weak self] in + guard let self else { + return + } + self.resolvePeerByNameDisposable.set(nil) + } + disposable.set((resolveSignal + |> take(1) + |> mapToSignal { peer -> Signal in + return .single(peer?._asPeer()) + } + |> deliverOnMainQueue).start(next: { [weak view] peer in + guard let view, let component = view.component else { + return + } + if let peer = peer { + var navigation = navigation + if case .default = navigation { + if let peer = peer as? TelegramUser, peer.botInfo != nil { + navigation = .chat(textInputState: nil, subject: nil, peekData: nil) + } + } + self.openResolved(view: view, result: .peer(peer, navigation)) + } else { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + component.controller()?.present(textAlertController(context: component.context, updatedPresentationData: nil, title: nil, text: presentationData.strings.Resolve_ErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + })) + } + + func openHashtag(view: StoryItemSetContainerComponent.View, hashtag: String, peerName: String?) { + guard let component = view.component, let parentController = component.controller() else { + return + } + + let peerId = component.slice.peer.id + + var resolveSignal: Signal + if let peerName = peerName { + resolveSignal = component.context.engine.peers.resolvePeerByName(name: peerName) + |> mapToSignal { peer -> Signal in + if let peer = peer { + return .single(peer._asPeer()) + } else { + return .single(nil) + } + } + } else { + resolveSignal = component.context.account.postbox.loadedPeerWithId(peerId) + |> map(Optional.init) + } + var cancelImpl: (() -> Void)? + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { [weak parentController] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + parentController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + resolveSignal = resolveSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { [weak self] in + guard let self else { + return + } + self.resolvePeerByNameDisposable.set(nil) + } + self.resolvePeerByNameDisposable.set((resolveSignal + |> deliverOnMainQueue).start(next: { [weak view] peer in + guard let view, let component = view.component else { + return + } + guard let navigationController = component.controller()?.navigationController as? NavigationController else { + return + } + if !hashtag.isEmpty { + let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag) + navigationController.pushViewController(searchController) + } + })) + } + + func openPeerMention(view: StoryItemSetContainerComponent.View, peerId: EnginePeer.Id) { + guard let component = view.component else { + return + } + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self, weak view] peer in + guard let self, let view, let peer else { + return + } + self.openPeer(view: view, peer: peer) + }) + } + + func openPeer(view: StoryItemSetContainerComponent.View, peer: EnginePeer, expandAvatar: Bool = false, peerTypes: ReplyMarkupButtonAction.PeerTypes? = nil) { + guard let component = view.component else { + return + } + + let peerSignal: Signal = component.context.account.postbox.loadedPeerWithId(peer.id) |> map(Optional.init) + self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak view] peer in + guard let view, let component = view.component, let peer else { + return + } + let mode: PeerInfoControllerMode = .generic + var expandAvatar = expandAvatar + if peer.smallProfileImage == nil { + expandAvatar = false + } + if component.metrics.widthClass == .regular { + expandAvatar = false + } + if let infoController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer, mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: false, requestsContext: nil) { + component.controller()?.push(infoController) + } + })) + } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift index 3872f92bd5..acbcf89799 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift @@ -1073,6 +1073,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { listContext.state, self.focusedIdUpdated.get() ) + //|> delay(0.4, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] peerAndVoiceMessages, state, _ in guard let self else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift index d575b7b4b9..5500e1de51 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryItemContentComponent.swift @@ -148,7 +148,7 @@ final class StoryItemContentComponent: Component { return } - if case let .file(file) = currentMessageMedia, let peerReference = PeerReference(component.peer._asPeer()) { + if case let .file(file) = currentMessageMedia, let peerReference = PeerReference(component.peer._asPeer()), !"".isEmpty { if self.videoNode == nil { let videoNode = UniversalVideoNode( postbox: component.context.account.postbox, diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 5de648bb72..2e2c42c4fa 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -240,7 +240,8 @@ public final class StoryPeerListComponent: Component { }, updateView: { view, state, transition in (view as? StoryPeerListItemComponent.TransitionView)?.update(state: state, transition: transition) - } + }, + insertCloneTransitionView: nil )) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 65809ecbaa..973fa03cc4 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -697,8 +697,9 @@ public final class StoryPeerListItemComponent: Component { var titleTransition = transition if previousComponent?.ringAnimation != nil && component.ringAnimation == nil { - if let titleView = self.title.view, let snapshotView = titleView.snapshotContentTree() { - titleView.superview?.addSubview(snapshotView) + if let titleView = self.title.view, let snapshotView = titleView.snapshotView(afterScreenUpdates: false) { + self.button.addSubview(snapshotView) + snapshotView.frame = titleView.frame snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/IconForwardSend.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/IconForwardSend.imageset/Contents.json new file mode 100644 index 0000000000..5c235cd01d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/IconForwardSend.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrowshape_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/IconForwardSend.imageset/arrowshape_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/IconForwardSend.imageset/arrowshape_30.pdf new file mode 100644 index 0000000000..752e9f0376 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/IconForwardSend.imageset/arrowshape_30.pdf @@ -0,0 +1,167 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.866699 4.701172 cm +0.000000 0.000000 0.000000 scn +11.694376 14.486328 m +11.694376 13.656328 l +12.152772 13.656328 12.524376 14.027931 12.524376 14.486328 c +11.694376 14.486328 l +h +11.694376 6.236328 m +12.524376 6.236328 l +12.524376 6.456457 12.436930 6.667571 12.281274 6.823226 c +12.125619 6.978882 11.914505 7.066328 11.694376 7.066328 c +11.694376 6.236328 l +h +1.306852 1.959093 m +0.679964 2.503071 l +1.306852 1.959093 l +h +0.098354 2.362061 m +-0.727694 2.442957 l +0.098354 2.362061 l +h +13.546710 1.785391 m +12.979038 2.390907 l +13.546710 1.785391 l +h +21.838385 11.163817 m +22.406055 11.769334 l +21.838385 11.163817 l +h +21.838385 9.558837 m +22.406055 8.953321 l +21.838385 9.558837 l +h +12.524376 14.486328 m +12.524376 18.134773 l +10.864376 18.134773 l +10.864376 14.486328 l +12.524376 14.486328 l +h +12.979040 18.331747 m +21.270714 10.558302 l +22.406055 11.769334 l +14.114382 19.542780 l +12.979040 18.331747 l +h +21.270712 10.164352 m +12.979038 2.390907 l +14.114381 1.179874 l +22.406055 8.953321 l +21.270712 10.164352 l +h +12.524376 2.587883 m +12.524376 6.236328 l +10.864376 6.236328 l +10.864376 2.587883 l +12.524376 2.587883 l +h +11.694376 7.066328 m +5.780328 7.066328 2.387692 4.471083 0.679964 2.503071 c +1.933740 1.415115 l +3.380624 3.082527 6.338683 5.406328 11.694376 5.406328 c +11.694376 7.066328 l +h +0.924402 2.281166 m +1.118631 4.264515 1.688284 7.135999 3.270899 9.490906 c +4.820140 11.796155 7.367803 13.656328 11.694376 13.656328 c +11.694376 15.316328 l +6.783209 15.316328 3.732595 13.153936 1.893128 10.416837 c +0.087034 7.729396 -0.522455 4.538746 -0.727694 2.442957 c +0.924402 2.281166 l +h +0.679964 2.503071 m +0.721193 2.550583 0.766227 2.565825 0.782870 2.569241 c +0.790647 2.570837 0.793910 2.570457 0.793002 2.570513 c +0.792788 2.570526 0.792269 2.570576 0.791612 2.570700 c +0.790952 2.570822 0.790647 2.570927 0.790832 2.570868 c +0.791008 2.570812 0.792187 2.570429 0.794370 2.569412 c +0.796545 2.568398 0.800247 2.566505 0.805242 2.563288 c +0.814777 2.557144 0.832552 2.543856 0.852494 2.519463 c +0.873059 2.494308 0.894952 2.458349 0.909617 2.412121 c +0.924611 2.364859 0.928116 2.319090 0.924402 2.281166 c +-0.727694 2.442957 l +-0.807448 1.628555 -0.199440 1.144114 0.287034 0.989164 c +0.769746 0.835413 1.467871 0.878242 1.933740 1.415115 c +0.679964 2.503071 l +h +12.979038 2.390907 m +12.806601 2.229246 12.524376 2.351511 12.524376 2.587883 c +10.864376 2.587883 l +10.864376 0.898287 12.881754 0.024286 14.114381 1.179874 c +12.979038 2.390907 l +h +21.270714 10.558302 m +21.384493 10.451633 21.384495 10.271024 21.270712 10.164352 c +22.406055 8.953321 l +23.219378 9.715812 23.219381 11.006841 22.406055 11.769334 c +21.270714 10.558302 l +h +12.524376 18.134773 m +12.524376 18.371141 12.806600 18.493410 12.979040 18.331747 c +14.114382 19.542780 l +12.881757 20.698366 10.864376 19.824371 10.864376 18.134773 c +12.524376 18.134773 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 2921 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000003011 00000 n +0000003034 00000 n +0000003207 00000 n +0000003281 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3340 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index cac2031ec8..328916515b 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -91,6 +91,9 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { let fromScale: CGFloat = 1.0 let scale = toScale.interpolate(to: fromScale, amount: state.progress) transition.setTransform(view: view, transform: CATransform3DMakeScale(scale, scale, 1.0)) + }, + insertCloneTransitionView: { view in + params.addToTransitionSurface(view) } ), destinationRect: selectedTransitionNode.1, diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 772e939e54..b50af35ce4 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -37,6 +37,7 @@ import TextFormat import ChatTextLinkEditUI import AttachmentTextInputPanelNode import ChatEntityKeyboardInputNode +import HashtagSearchUI private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -1718,6 +1719,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return inputPanelNode } + public func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String) -> ViewController { + return HashtagSearchController(context: context, peer: peer, query: query) + } + public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource) -> ViewController { let mappedSource: PremiumSource switch source { diff --git a/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift index 758aa60e85..0257a9b038 100644 --- a/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift @@ -140,6 +140,9 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { text = strings.Message_Theme } else if content.type == "video" { text = stringForMediaKind(.video, strings: self.strings).0.string + } else if content.type == "telegram_story" { + //TODO:localize + text = "Story" } else if let _ = content.image { text = stringForMediaKind(.image, strings: self.strings).0.string }