From 04b25d7152cc75def428da99bdb9dce39418a453 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 22 Oct 2024 14:09:47 +0400 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 9 + .../Sources/AccountContext.swift | 2 +- .../Sources/ChatController.swift | 6 +- .../ChatListUI/Sources/ChatContextMenus.swift | 17 +- submodules/HashtagSearchUI/BUILD | 4 + .../Sources/HashtagSearchController.swift | 17 +- .../Sources/HashtagSearchControllerNode.swift | 235 +++++++++++++++--- .../Sources/StoryResultsPanelComponent.swift | 175 +++++++++---- .../Sources/LegacyAttachmentMenu.swift | 2 +- .../Components/AnimatedTextComponent/BUILD | 1 + .../Sources/AnimatedTextComponent.swift | 49 +++- .../PeerInfoScreen/Sources/PeerInfoData.swift | 8 +- .../Sources/PeerInfoStoryPaneNode.swift | 19 +- ...StoryItemSetContainerViewSendMessage.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 15 +- ...rollerOpenMessageReactionContextMenu.swift | 30 +-- .../Sources/ChatControllerUpdateSearch.swift | 5 + .../Sources/ChatTagSearchInputPanelNode.swift | 74 +++--- .../Sources/SharedAccountContext.swift | 4 +- .../TranslateUI/Sources/ChatTranslation.swift | 19 +- .../WebUI/Sources/WebAppController.swift | 9 + 21 files changed, 516 insertions(+), 186 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 0677a3bd71..b5a503f9f5 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11127,6 +11127,9 @@ Sorry for the inconvenience."; "Chat.BottomSearchPanel.MessageCount_1" = "message"; "Chat.BottomSearchPanel.MessageCount_any" = "messages"; +"Chat.BottomSearchPanel.StoryCount_1" = "story"; +"Chat.BottomSearchPanel.StoryCount_any" = "stories"; + "Chat.BottomSearchPanel.DisplayModeFormat" = "Show as %@"; "Chat.BottomSearchPanel.DisplayModeChat" = "Chat"; "Chat.BottomSearchPanel.DisplayModeList" = "List"; @@ -12397,6 +12400,12 @@ Sorry for the inconvenience."; "HashtagSearch.Stories_any" = "%@ Stories"; "HashtagSearch.LocalStoriesFound" = "%1$@ in %2$@"; +"HashtagSearch.Posts_1" = "%@ Message"; +"HashtagSearch.Posts_any" = "%@ Messages"; +"HashtagSearch.FoundInfoFormat" = "View %1$@ with %2$@"; +"HashtagSearch.FoundStories" = "stories"; +"HashtagSearch.FoundPosts" = "posts"; + "Stars.BotRevenue.Title" = "Stars Balance"; "Stars.BotRevenue.Revenue.Title" = "Stars Received"; "Stars.BotRevenue.Proceeds.Title" = "Rewards Overview"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index aa4ffd7267..31431ed3c2 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -941,7 +941,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, isScheduledMessages: Bool, isFile: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? - func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController + func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController func makeArchiveSettingsController(context: AccountContext) -> ViewController diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index b2dd59e404..21d5f1d820 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -1031,14 +1031,18 @@ public protocol ChatController: ViewController { var visibleContextController: ViewController? { get } + var contentContainerNode: ASDisplayNode { get } + var searching: ValuePromise { get } + var searchResultsCount: ValuePromise { get } + var externalSearchResultsCount: Int32? { get set } var alwaysShowSearchResultsAsList: Bool { get set } var includeSavedPeersInSearchResults: Bool { get set } var showListEmptyResults: Bool { get set } + func beginMessageSearch(_ query: String) func updatePresentationMode(_ mode: ChatControllerPresentationMode) - func beginMessageSearch(_ query: String) func displayPromoAnnouncement(text: String) func updatePushedTransition(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index b710deca99..3a93d96776 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -253,6 +253,13 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in var updatedItems: [ContextMenuItem] = [] + updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { c, _ in + c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) + }))) + updatedItems.append(.separator) + for filter in filters { if case let .filter(_, title, _, data) = filter { let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId) @@ -338,16 +345,10 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch }))) } } - - updatedItems.append(.separator) - updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) - }, iconPosition: .left, action: { c, _ in - c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - + c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true) }))) + items.append(.separator) } } diff --git a/submodules/HashtagSearchUI/BUILD b/submodules/HashtagSearchUI/BUILD index 79973de2e0..eecdc4a889 100644 --- a/submodules/HashtagSearchUI/BUILD +++ b/submodules/HashtagSearchUI/BUILD @@ -28,6 +28,10 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/Components/BlurredBackgroundComponent", + "//submodules/UIKitRuntimeUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index facf7454bc..811950283c 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -25,6 +25,8 @@ public final class HashtagSearchController: TelegramBaseController { private let query: String let mode: Mode let publicPosts: Bool + let stories: Bool + let forceDark: Bool private var transitionDisposable: Disposable? private let openMessageFromSearchDisposable = MetaDisposable() @@ -39,17 +41,23 @@ public final class HashtagSearchController: TelegramBaseController { return self.displayNode as! HashtagSearchControllerNode } - public init(context: AccountContext, peer: EnginePeer?, query: String, mode: Mode = .generic, publicPosts: Bool = false) { + public init(context: AccountContext, peer: EnginePeer?, query: String, mode: Mode = .generic, publicPosts: Bool = false, stories: Bool = false, forceDark: Bool = false) { self.context = context self.peer = peer self.query = query self.mode = mode self.publicPosts = publicPosts + self.stories = stories + self.forceDark = forceDark self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if forceDark { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + self.presentationData = presentationData super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none, groupCallPanelSource: .none) @@ -69,6 +77,11 @@ public final class HashtagSearchController: TelegramBaseController { let previousTheme = self.presentationData.theme let previousStrings = self.presentationData.strings + var presentationData = presentationData + if forceDark { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + self.presentationData = presentationData if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift index e43afdcdbd..2da8cbbe44 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift @@ -9,6 +9,8 @@ import AccountContext import ChatListUI import SegmentedControlNode import ChatListSearchItemHeader +import PeerInfoVisualMediaPaneNode +import UIKitRuntimeUtils final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { private let context: AccountContext @@ -30,6 +32,9 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg private let isSearching = Promise() private var isSearchingDisposable: Disposable? + private var searchResultsCount: Int32 = 0 + private var searchResultsCountDisposable: Disposable? + private let clippingNode: ASDisplayNode private let containerNode: ASDisplayNode let currentController: ChatController? @@ -39,10 +44,13 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg let globalController: ChatController? let globalChatContents: HashtagSearchGlobalChatContents? - private var globalStorySearchContext: SearchStoryListContext? - private var globalStorySearchDisposable = MetaDisposable() - private var globalStorySearchState: StoryListContext.State? - private var globalStorySearchComponentView: ComponentView? + private var storySearchContext: SearchStoryListContext? + private var storySearchDisposable = MetaDisposable() + private var storySearchState: StoryListContext.State? + private var storySearchComponentView: ComponentView? + + private var storySearchPaneNode: PeerInfoStoryPaneNode? + private var isDisplayingStories = false private var panRecognizer: InteractiveTransitionGestureRecognizer? @@ -57,8 +65,14 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.navigationBar = navigationBar self.isCashtag = query.hasPrefix("$") self.presentationData = controller.presentationData + self.isDisplayingStories = controller.stories - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + var controllerParams: ChatControllerParams? + if controller.forceDark { + controllerParams = ChatControllerParams(forcedTheme: defaultDarkColorPresentationTheme, forcedWallpaper: defaultBuiltinWallpaper(data: .default, colors: defaultDarkWallpaperGradientColors.map(\.rgb), intensity: -34)) + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true @@ -78,7 +92,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg let navigationController = controller.navigationController as? NavigationController if let peer, controller.mode != .noChat { - self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController), params: nil) + self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController), params: controllerParams) self.currentController?.alwaysShowSearchResultsAsList = true self.currentController?.showListEmptyResults = true self.currentController?.customNavigationController = navigationController @@ -89,7 +103,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg if let _ = peer, controller.mode != .chatOnly { let myChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: false) self.myChatContents = myChatContents - self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default), params: nil) + self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default), params: controllerParams) self.myController?.alwaysShowSearchResultsAsList = true self.myController?.showListEmptyResults = true self.myController?.customNavigationController = navigationController @@ -100,7 +114,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg let globalChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: true) self.globalChatContents = globalChatContents - self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default), params: nil) + self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default), params: controllerParams) self.globalController?.alwaysShowSearchResultsAsList = true self.globalController?.showListEmptyResults = true self.globalController?.customNavigationController = navigationController @@ -182,7 +196,9 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg navigationBar?.setContentNode(self.searchContentNode, animated: false) } - self.addSubnode(self.shimmerNode) + if !self.isDisplayingStories { + self.addSubnode(self.shimmerNode) + } self.searchContentNode.setQueryUpdated { [weak self] query in self?.searchQueryPromise.set(query) @@ -227,13 +243,25 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } }) + if let currentController = self.currentController { + self.searchResultsCountDisposable = (currentController.searchResultsCount.get() + |> deliverOnMainQueue).start(next: { [weak self] searchResultsCount in + guard let self else { + return + } + self.searchResultsCount = searchResultsCount + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + }) + } + self.updateStorySearch() } deinit { self.searchQueryDisposable?.dispose() self.isSearchingDisposable?.dispose() - self.globalStorySearchDisposable.dispose() + self.searchResultsCountDisposable?.dispose() + self.storySearchDisposable.dispose() } private var panAllowedDirections: InteractiveTransitionGestureRecognizerDirections { @@ -373,29 +401,30 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } private func updateStorySearch() { - self.globalStorySearchState = nil - self.globalStorySearchDisposable.set(nil) - self.globalStorySearchContext = nil + self.storySearchState = nil + self.storySearchDisposable.set(nil) + self.storySearchContext = nil if !self.query.isEmpty { var peerId: EnginePeer.Id? if self.controller?.mode == .chatOnly { peerId = self.peer?.id } - let globalStorySearchContext = SearchStoryListContext(account: self.context.account, source: .hashtag(peerId, self.query)) - self.globalStorySearchDisposable.set((globalStorySearchContext.state + let storySearchContext = SearchStoryListContext(account: self.context.account, source: .hashtag(peerId, self.query)) + self.storySearchDisposable.set((storySearchContext.state |> deliverOnMainQueue).startStrict(next: { [weak self] state in guard let self else { return } if state.totalCount > 0 { - self.globalStorySearchState = state + self.storySearchState = state } else { - self.globalStorySearchState = nil + self.storySearchState = nil + self.currentController?.externalSearchResultsCount = nil } - self.requestUpdate(transition: .animated(duration: 0.25, curve: .easeInOut)) + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) })) - self.globalStorySearchContext = globalStorySearchContext + self.storySearchContext = storySearchContext } } @@ -420,6 +449,37 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } } + private func animateContentOut() { + guard let controller = self.currentController else { + return + } + controller.contentContainerNode.layer.animateSublayerScale(from: 1.0, to: 0.95, duration: 0.3, removeOnCompletion: false) + + if let blurFilter = makeBlurFilter() { + blurFilter.setValue(30.0 as NSNumber, forKey: "inputRadius") + controller.contentContainerNode.layer.filters = [blurFilter] + controller.contentContainerNode.layer.animate(from: 0.0 as NSNumber, to: 30.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: false) + } + } + + private func animateContentIn() { + guard let controller = self.currentController else { + return + } + controller.contentContainerNode.layer.animateSublayerScale(from: 0.95, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + + if let blurFilter = makeBlurFilter() { + blurFilter.setValue(0.0 as NSNumber, forKey: "inputRadius") + controller.contentContainerNode.layer.filters = [blurFilter] + controller.contentContainerNode.layer.animate(from: 30.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak controller] completed in + guard let controller, completed else { + return + } + controller.contentContainerNode.layer.filters = [] + }) + } + } + func requestUpdate(transition: ContainedViewLayoutTransition) { if let (layout, navigationHeight) = self.containerLayout { let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition) @@ -440,45 +500,69 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.insertSubnode(self.clippingNode, at: 0) } + var storyParentController: ViewController? + if self.controller?.mode == .chatOnly { + storyParentController = self.currentController + } else { + storyParentController = self.globalController + } + var currentTopInset: CGFloat = 0.0 var globalTopInset: CGFloat = 0.0 - if let state = self.globalStorySearchState { - var parentController: ViewController? - if self.controller?.mode == .chatOnly { - parentController = self.currentController + + var panelSearchState: StoryResultsPanelComponent.SearchState? + if let storySearchState = self.storySearchState { + if self.isDisplayingStories { + if self.searchResultsCount > 0 { + panelSearchState = .messages(self.searchResultsCount) + } } else { - parentController = self.globalController + panelSearchState = .stories(storySearchState) } - if let parentController { + } + + if self.isDisplayingStories { + if let storySearchState = self.storySearchState { + self.currentController?.externalSearchResultsCount = Int32(storySearchState.totalCount) + } else { + self.currentController?.externalSearchResultsCount = nil + } + } else { + self.currentController?.externalSearchResultsCount = nil + } + + if let panelSearchState { + if let storyParentController { let componentView: ComponentView var panelTransition = ComponentTransition(transition) - if let current = self.globalStorySearchComponentView { + if let current = self.storySearchComponentView { componentView = current } else { panelTransition = .immediate componentView = ComponentView() - self.globalStorySearchComponentView = componentView + self.storySearchComponentView = componentView } let panelSize = componentView.update( - transition: .immediate, + transition: panelTransition, component: AnyComponent(StoryResultsPanelComponent( context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, query: self.query, peer: self.controller?.mode == .chatOnly ? self.peer : nil, - state: state, + state: panelSearchState, sideInset: layout.safeInsets.left, action: { [weak self] in guard let self else { return } - var peer: EnginePeer? if self.controller?.mode == .chatOnly { - peer = self.peer + self.isDisplayingStories = !self.isDisplayingStories + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + } else { + let searchController = self.context.sharedContext.makeStorySearchController(context: self.context, scope: .query(nil, self.query), listContext: self.storySearchContext) + self.controller?.push(searchController) } - let searchController = self.context.sharedContext.makeStorySearchController(context: self.context, scope: .query(peer, self.query), listContext: self.globalStorySearchContext) - self.controller?.push(searchController) } )), environment: {}, @@ -487,8 +571,8 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top - 36.0), size: panelSize) if let view = componentView.view { if view.superview == nil { - parentController.view.addSubview(view) - view.layer.animatePosition(from: CGPoint(x: 0.0, y: -panelSize.height), to: .zero, duration: 0.25, additive: true) + storyParentController.view.addSubview(view) + view.layer.animatePosition(from: CGPoint(x: 0.0, y: -panelSize.height), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } panelTransition.setFrame(view: view, frame: panelFrame) } @@ -498,9 +582,9 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg globalTopInset += panelSize.height } } - } else if let globalStorySearchComponentView = self.globalStorySearchComponentView { - globalStorySearchComponentView.view?.removeFromSuperview() - self.globalStorySearchComponentView = nil + } else if let storySearchComponentView = self.storySearchComponentView { + storySearchComponentView.view?.removeFromSuperview() + self.storySearchComponentView = nil } if let controller = self.currentController { @@ -548,6 +632,75 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } } + if self.isDisplayingStories, let peer = self.peer, let storySearchContext = self.storySearchContext { + let storySearchPaneNode: PeerInfoStoryPaneNode + var paneTransition = transition + if let current = self.storySearchPaneNode { + storySearchPaneNode = current + } else { + storySearchPaneNode = PeerInfoStoryPaneNode( + context: self.context, + scope: .search(peerId: peer.id, query: self.query), + captureProtected: false, + isProfileEmbedded: false, + canManageStories: false, + navigationController: { [weak self] in + guard let self else { + return nil + } + return self.controller?.navigationController as? NavigationController + }, + listContext: storySearchContext + ) + self.storySearchPaneNode = storySearchPaneNode + if let storySearchView = self.storySearchComponentView?.view { + storySearchView.superview?.insertSubview(storySearchPaneNode.view, belowSubview: storySearchView) + } else { + storyParentController?.view.addSubview(storySearchPaneNode.view) + } + paneTransition = .immediate + + if transition.isAnimated { + storySearchPaneNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + storySearchPaneNode.layer.animateSublayerScale(from: 0.95, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + self.animateContentOut() + } + } + + var bottomInset: CGFloat = 0.0 + if case .regular = layout.metrics.widthClass { + bottomInset += 49.0 + } else { + bottomInset += 45.0 + } + bottomInset += layout.intrinsicInsets.bottom + + storySearchPaneNode.update( + size: layout.size, + topInset: navigationBarHeight, + sideInset: layout.safeInsets.left, + bottomInset: 0.0, + deviceMetrics: layout.deviceMetrics, + visibleHeight: layout.size.height - currentTopInset, + isScrollingLockedAtTop: false, + expandProgress: 1.0, + navigationHeight: 0.0, + presentationData: self.presentationData, + synchronous: false, + transition: paneTransition + ) + storySearchPaneNode.view.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + paneTransition.updateFrame(node: storySearchPaneNode, frame: CGRect(origin: CGPoint(x: 0.0, y: currentTopInset), size: CGSize(width: layout.size.width, height: layout.size.height - bottomInset - currentTopInset))) + } else if let storySearchPaneNode = self.storySearchPaneNode { + self.storySearchPaneNode = nil + + storySearchPaneNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + storySearchPaneNode.view.removeFromSuperview() + }) + storySearchPaneNode.layer.animateSublayerScale(from: 1.0, to: 0.95, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + self.animateContentIn() + } + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: .zero, size: layout.size)) let containerPosition: CGFloat = -layout.size.width * CGFloat(self.searchContentNode.selectedIndex) - self.panTransitionFraction * layout.size.width @@ -559,7 +712,11 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.shimmerNode.update(context: self.context, size: CGSize(width: layout.size.width - overflowInset * 2.0, height: layout.size.height), presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, key: .chats, hasSelection: false, transition: transition) if isFirstTime { - self.insertSubnode(self.recentListNode, aboveSubnode: self.shimmerNode) + if self.shimmerNode.supernode != nil { + self.insertSubnode(self.recentListNode, aboveSubnode: self.shimmerNode) + } else { + self.insertSubnode(self.recentListNode, aboveSubnode: self.clippingNode) + } } transition.updateFrame(node: self.recentListNode, frame: CGRect(origin: .zero, size: layout.size)) diff --git a/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift b/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift index 507f743db4..6ee748f476 100644 --- a/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift +++ b/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift @@ -7,14 +7,20 @@ import MultilineTextComponent import BundleIconComponent import StorySetIndicatorComponent import AccountContext +import AnimatedTextComponent +import BlurredBackgroundComponent final class StoryResultsPanelComponent: CombinedComponent { + enum SearchState: Equatable { + case stories(StoryListContext.State) + case messages(Int32) + } let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let query: String let peer: EnginePeer? - let state: StoryListContext.State + let state: SearchState let sideInset: CGFloat let action: () -> Void @@ -24,7 +30,7 @@ final class StoryResultsPanelComponent: CombinedComponent { strings: PresentationStrings, query: String, peer: EnginePeer?, - state: StoryListContext.State, + state: SearchState, sideInset: CGFloat, action: @escaping () -> Void ) { @@ -61,10 +67,11 @@ final class StoryResultsPanelComponent: CombinedComponent { } static var body: Body { - let background = Child(Rectangle.self) + let background = Child(BlurredBackgroundComponent.self) let avatars = Child(StorySetIndicatorComponent.self) + let titlePrefix = Child(AnimatedTextComponent.self) let title = Child(MultilineTextComponent.self) - let text = Child(MultilineTextComponent.self) + let text = Child(AnimatedTextComponent.self) let arrow = Child(BundleIconComponent.self) let separator = Child(Rectangle.self) let button = Child(Button.self) @@ -74,39 +81,47 @@ final class StoryResultsPanelComponent: CombinedComponent { let spacing: CGFloat = 3.0 - let textLeftInset: CGFloat = 81.0 + component.sideInset + var textLeftInset: CGFloat = 16.0 + component.sideInset let textTopInset: CGFloat = 9.0 var existingPeerIds = Set() var items: [StorySetIndicatorComponent.Item] = [] - for item in component.state.items { - guard let peer = item.peer, !existingPeerIds.contains(peer.id) else { - continue + switch component.state { + case let .stories(state): + for item in state.items { + guard let peer = item.peer, !existingPeerIds.contains(peer.id) || component.peer != nil else { + continue + } + existingPeerIds.insert(peer.id) + items.append(StorySetIndicatorComponent.Item(storyItem: item.storyItem, peer: peer)) } - existingPeerIds.insert(peer.id) - items.append(StorySetIndicatorComponent.Item(storyItem: item.storyItem, peer: peer)) + textLeftInset += 65.0 + default: + break } - - let avatars = avatars.update( - component: StorySetIndicatorComponent( - context: component.context, - strings: component.strings, - items: Array(items.prefix(3)), - displayAvatars: true, - hasUnseen: true, - hasUnseenPrivate: false, - totalCount: 0, - theme: component.theme, - action: {} - ), - availableSize: context.availableSize, - transition: .immediate - ) - + + var titlePrefixString: [AnimatedTextComponent.Item] = [] let titleString: NSAttributedString + var textString: [AnimatedTextComponent.Item] = [] if let peer = component.peer, let username = peer.addressName { - let storiesString = component.strings.HashtagSearch_Stories(Int32(component.state.totalCount)) - let fullString = component.strings.HashtagSearch_LocalStoriesFound(storiesString, "@\(username)") + let entityType: String + switch component.state { + case let .messages(count): + titlePrefixString = [AnimatedTextComponent.Item( + id: "text", + isUnbreakable: true, + content: .text(component.strings.HashtagSearch_Posts(count)) + )] + entityType = component.strings.HashtagSearch_FoundPosts + case let .stories(state): + titlePrefixString = [AnimatedTextComponent.Item( + id: "text", + isUnbreakable: true, + content: .text(component.strings.HashtagSearch_Stories(Int32(state.totalCount))) + )] + entityType = component.strings.HashtagSearch_FoundStories + } + let fullString = component.strings.HashtagSearch_LocalStoriesFound("", "@\(username)") titleString = NSMutableAttributedString( string: fullString.string, font: Font.semibold(15.0), @@ -116,13 +131,48 @@ final class StoryResultsPanelComponent: CombinedComponent { if let lastRange = fullString.ranges.last?.range { (titleString as? NSMutableAttributedString)?.addAttribute(NSAttributedString.Key.foregroundColor, value: component.theme.rootController.navigationBar.accentTextColor, range: lastRange) } + textString = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.HashtagSearch_FoundInfoFormat( + ".", + "." + ), id: "info", mapping: [ + 0: .text(entityType), + 1: .text(component.query) + ]) } else { - titleString = NSAttributedString( - string: component.strings.HashtagSearch_StoriesFound(Int32(component.state.totalCount)), - font: Font.semibold(15.0), - textColor: component.theme.rootController.navigationBar.primaryTextColor, - paragraphAlignment: .natural + if case let .stories(state) = component.state { + titleString = NSAttributedString( + string: component.strings.HashtagSearch_StoriesFound(Int32(state.totalCount)), + font: Font.semibold(15.0), + textColor: component.theme.rootController.navigationBar.primaryTextColor, + paragraphAlignment: .natural + ) + } else { + titleString = NSAttributedString() + } + textString = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.HashtagSearch_FoundInfoFormat( + ".", + "." + ), id: "info", mapping: [ + 0: .text(component.strings.HashtagSearch_FoundStories), + 1: .text(component.query) + ]) + } + + var titlePrefixOffset: CGFloat = 0.0 + var titlePrefixChild: _UpdatedChildComponent? + if !titlePrefixString.isEmpty { + let titlePrefix = titlePrefix.update( + component: AnimatedTextComponent( + font: Font.semibold(15.0), + color: component.theme.rootController.navigationBar.primaryTextColor, + items: titlePrefixString, + noDelay: true + ), + availableSize: CGSize(width: context.availableSize.width - textLeftInset - 64.0, height: context.availableSize.height), + transition: context.transition ) + titlePrefixOffset = titlePrefix.size.width + 1.0 + titlePrefixChild = titlePrefix } let title = title.update( @@ -131,23 +181,19 @@ final class StoryResultsPanelComponent: CombinedComponent { horizontalAlignment: .natural, maximumNumberOfLines: 1 ), - availableSize: CGSize(width: context.availableSize.width - textLeftInset, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate + availableSize: CGSize(width: context.availableSize.width - textLeftInset - 64.0 - titlePrefixOffset, height: CGFloat.greatestFiniteMagnitude), + transition: context.transition ) - + let text = text.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.strings.HashtagSearch_StoriesFoundInfo(component.query).string, - font: Font.regular(14.0), - textColor: component.theme.rootController.navigationBar.secondaryTextColor, - paragraphAlignment: .natural - )), - horizontalAlignment: .natural, - maximumNumberOfLines: 1 + component: AnimatedTextComponent( + font: Font.regular(14.0), + color: component.theme.rootController.navigationBar.secondaryTextColor, + items: textString, + noDelay: true ), availableSize: CGSize(width: context.availableSize.width - textLeftInset, height: context.availableSize.height), - transition: .immediate + transition: context.transition ) let arrow = arrow.update( @@ -162,7 +208,7 @@ final class StoryResultsPanelComponent: CombinedComponent { let size = CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + spacing + text.size.height + textTopInset + 2.0) let background = background.update( - component: Rectangle(color: component.theme.rootController.navigationBar.opaqueBackgroundColor), + component: BlurredBackgroundComponent(color: component.theme.rootController.navigationBar.blurredBackgroundColor), availableSize: size, transition: .immediate ) @@ -190,12 +236,37 @@ final class StoryResultsPanelComponent: CombinedComponent { .position(CGPoint(x: background.size.width / 2.0, y: background.size.height - separator.size.height / 2.0)) ) - context.add(avatars - .position(CGPoint(x: component.sideInset + 10.0 + 30.0, y: background.size.height / 2.0)) - ) + if !items.isEmpty { + let avatars = avatars.update( + component: StorySetIndicatorComponent( + context: component.context, + strings: component.strings, + items: Array(items.prefix(3)), + displayAvatars: component.peer == nil, + hasUnseen: true, + hasUnseenPrivate: false, + totalCount: 0, + theme: component.theme, + action: {} + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(avatars + .position(CGPoint(x: component.sideInset + 10.0 + 30.0, y: background.size.height / 2.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } + if let titlePrefixChild { + context.add(titlePrefixChild + .position(CGPoint(x: textLeftInset + titlePrefixChild.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + } + context.add(title - .position(CGPoint(x: textLeftInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + .position(CGPoint(x: textLeftInset + titlePrefixOffset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) ) context.add(text diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index c355b93bff..ea428cb7bb 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -565,7 +565,7 @@ public func legacyAttachmentMenu( carouselItemView?.underlyingViews = underlyingViews - if editMediaOptions == nil { + if editMediaOptions == nil && !addingMedia { for i in 0 ..< min(20, recentlyUsedInlineBots.count) { let peer = recentlyUsedInlineBots[i] let addressName = peer.addressName diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD b/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD index 53ec1414a6..f4ce434cc1 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift index bb26607690..b3ead20bb7 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import ComponentFlow +import TelegramPresentationData public final class AnimatedTextComponent: Component { public struct Item: Equatable { @@ -24,15 +25,18 @@ public final class AnimatedTextComponent: Component { public let font: UIFont public let color: UIColor public let items: [Item] + public let noDelay: Bool public init( font: UIFont, color: UIColor, - items: [Item] + items: [Item], + noDelay: Bool = false ) { self.font = font self.color = color self.items = items + self.noDelay = noDelay } public static func ==(lhs: AnimatedTextComponent, rhs: AnimatedTextComponent) -> Bool { @@ -45,6 +49,9 @@ public final class AnimatedTextComponent: Component { if lhs.items != rhs.items { return false } + if lhs.noDelay != rhs.noDelay { + return false + } return true } @@ -157,10 +164,12 @@ public final class AnimatedTextComponent: Component { if animateIn, !transition.animation.isImmediate { var delayWidth: Double = 0.0 - if let firstDelayWidth { - delayWidth = size.width - firstDelayWidth - } else { - firstDelayWidth = size.width + if !component.noDelay { + if let firstDelayWidth { + delayWidth = size.width - firstDelayWidth + } else { + firstDelayWidth = size.width + } } characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring) @@ -220,3 +229,33 @@ public final class AnimatedTextComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +public extension AnimatedTextComponent { + static func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] { + var textItems: [AnimatedTextComponent.Item] = [] + + var previousIndex = 0 + let nsString = string.string as NSString + for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { + if range.range.lowerBound > previousIndex { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) + } + if let value = mapping[range.index] { + let isUnbreakable: Bool + switch value { + case .text: + isUnbreakable = true + case .number: + isUnbreakable = false + } + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value)) + } + previousIndex = range.range.upperBound + } + if nsString.length > previousIndex { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex))))) + } + + return textItems + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 5b51aec255..16eafe257e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -1025,7 +1025,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let premiumGiftOptions: Signal<[PremiumGiftCodeOption], NoError> let profileGiftsContext: ProfileGiftsContext? if case .user = kind { - profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: userPeerId) + if isMyProfile || userPeerId != context.account.peerId { + profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: userPeerId) + } else { + profileGiftsContext = nil + } premiumGiftOptions = .single([]) |> then( context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) @@ -1314,7 +1318,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen availablePanes?.insert(.stories, at: 0) } - if availablePanes != nil, profileGiftsContext != nil, let cachedData = peerView.cachedData as? CachedUserData { + if availablePanes != nil, profileGiftsContext != nil, let cachedData = peerView.cachedData as? CachedUserData, peerView.peerId != context.account.peerId { if let starGiftsCount = cachedData.starGiftsCount, starGiftsCount > 0 { availablePanes?.insert(.gifts, at: hasStories ? 1 : 0) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 7fd1a3f52e..f3c14f0fba 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1770,7 +1770,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if case .peer = self.scope { let _ = (ApplicationSpecificNotice.getSharedMediaScrollingTooltip(accountManager: context.sharedContext.accountManager) - |> deliverOnMainQueue).start(next: { [weak self] count in + |> deliverOnMainQueue).start(next: { [weak self] count in guard let strongSelf = self else { return } @@ -2420,7 +2420,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.updateDisposable.dispose() self.mapDisposable?.dispose() } - + public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal { let listSource = self.listSource return Signal { subscriber in @@ -2862,6 +2862,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr continue } + var authorPeer = item.peer var isReorderable = false switch self.scope { case .botPreview: @@ -2870,16 +2871,20 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if id == self.context.account.peerId { isReorderable = state.pinnedIds.contains(item.storyItem.id) } + case let .search(peerId, _): + if peerId != nil { + authorPeer = nil + } default: break } - + mappedItems.append(VisualMediaItem( index: mappedItems.count, peer: peerReference, storyId: item.id, story: item.storyItem, - authorPeer: item.peer, + authorPeer: authorPeer, isPinned: state.pinnedIds.contains(item.storyItem.id), localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue, isReorderable: isReorderable @@ -4104,7 +4109,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if self.isProfileEmbedded, case .botPreview = self.scope { self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor } else { - self.view.backgroundColor = .clear + if case let .search(peerId, _) = self.scope, peerId != nil { + + } else { + self.view.backgroundColor = .clear + } } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 6ffefdbfd4..4a54068985 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -2922,7 +2922,7 @@ final class StoryItemSetContainerSendMessage { let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, scope: .query(nil, hashtag), listContext: nil) navigationController.pushViewController(searchController) } else { - let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, all: true) + let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, stories: true, forceDark: true) navigationController.pushViewController(searchController) } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 285c77d8a5..ef9003f9aa 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -330,6 +330,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let startingBot = ValuePromise(false, ignoreRepeated: true) let unblockingPeer = ValuePromise(false, ignoreRepeated: true) public let searching = ValuePromise(false, ignoreRepeated: true) + public let searchResultsCount = ValuePromise(0, ignoreRepeated: true) let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>() let loadingMessage = Promise(nil) let performingInlineSearch = ValuePromise(false, ignoreRepeated: true) @@ -629,6 +630,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + public var externalSearchResultsCount: Int32? { + didSet { + if let panelNode = self.chatDisplayNode.inputPanelNode as? ChatTagSearchInputPanelNode { + panelNode.externalSearchResultsCount = self.externalSearchResultsCount + } + } + } + public var includeSavedPeersInSearchResults: Bool = false { didSet { self.chatDisplayNode.includeSavedPeersInSearchResults = self.includeSavedPeersInSearchResults @@ -9697,7 +9706,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else if case let .customChatContents(contents) = self.subject, case let .hashTagSearch(publicPostsValue) = contents.kind { publicPosts = publicPostsValue } - let searchController = HashtagSearchController(context: self.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, mode: peerName != nil ? .chatOnly : .generic, publicPosts: publicPosts) + let searchController = HashtagSearchController(context: self.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, mode: peerName != nil ? .chatOnly : .generic, publicPosts: peerName == nil && publicPosts) self.effectiveNavigationController?.pushViewController(searchController) } })) @@ -10972,4 +10981,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return true } } + + public var contentContainerNode: ASDisplayNode { + return self.chatDisplayNode.contentContainerNode + } } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index 59b0a256ce..ee04acb0df 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -513,7 +513,7 @@ extension ChatControllerImpl { title = self.presentationData.strings.Chat_ToastStarsSent_Title(Int32(self.currentSendStarsUndoCount)) } - let textItems = extractAnimatedTextString(string: self.presentationData.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [ + let textItems = AnimatedTextComponent.extractAnimatedTextString(string: self.presentationData.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [ 0: .number(self.currentSendStarsUndoCount, minDigits: 1), 1: .text(self.presentationData.strings.Chat_ToastStarsSent_TextStarAmount(Int32(self.currentSendStarsUndoCount))) ]) @@ -536,31 +536,3 @@ extension ChatControllerImpl { } } } - -private func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] { - var textItems: [AnimatedTextComponent.Item] = [] - - var previousIndex = 0 - let nsString = string.string as NSString - for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { - if range.range.lowerBound > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) - } - if let value = mapping[range.index] { - let isUnbreakable: Bool - switch value { - case .text: - isUnbreakable = true - case .number: - isUnbreakable = false - } - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value)) - } - previousIndex = range.range.upperBound - } - if nsString.length > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex))))) - } - - return textItems -} diff --git a/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift b/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift index 59f3755db3..07cd1d573d 100644 --- a/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift @@ -77,6 +77,7 @@ extension ChatControllerImpl { if queryIsEmpty { self.searching.set(false) + self.searchResultsCount.set(0) self.searchDisposable?.set(nil) self.searchResult.set(.single(nil)) if let data = interfaceState.search { @@ -104,6 +105,7 @@ extension ChatControllerImpl { guard let strongSelf = self else { return } + strongSelf.searchResultsCount.set(results.totalCount) var navigateIndex: MessageIndex? strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in if let data = current.search { @@ -152,6 +154,7 @@ extension ChatControllerImpl { guard let strongSelf = self else { return } + strongSelf.searchResultsCount.set(results.totalCount) strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in if let data = current.search, let previousResultsState = data.resultsState { let messageIndices = results.messages.map({ $0.index }).sorted() @@ -167,11 +170,13 @@ extension ChatControllerImpl { })) } else { self.searching.set(false) + self.searchResultsCount.set(0) self.searchDisposable?.set(nil) } } } else { self.searching.set(false) + self.searchResultsCount.set(0) self.searchDisposable?.set(nil) if let data = interfaceState.search { diff --git a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift index 8a2fbb0757..aa3a571d83 100644 --- a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift @@ -19,34 +19,6 @@ import AnimatedTextComponent private let labelFont = Font.regular(15.0) -private func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] { - var textItems: [AnimatedTextComponent.Item] = [] - - var previousIndex = 0 - let nsString = string.string as NSString - for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { - if range.range.lowerBound > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) - } - if let value = mapping[range.index] { - let isUnbreakable: Bool - switch value { - case .text: - isUnbreakable = true - case .number: - isUnbreakable = false - } - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value)) - } - previousIndex = range.range.upperBound - } - if nsString.length > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex))))) - } - - return textItems -} - final class ChatTagSearchInputPanelNode: ChatInputPanelNode { private struct Params: Equatable { var width: CGFloat @@ -100,6 +72,14 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { private var totalMessageCount: Int? private var totalMessageCountDisposable: Disposable? + public var externalSearchResultsCount: Int32? { + didSet { + if let params = self.currentLayout?.params { + let _ = self.update(params: params, transition: .spring(duration: 0.4)) + } + } + } + override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { } @@ -223,7 +203,15 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { var canChangeListMode = false var resultsTextString: [AnimatedTextComponent.Item] = [] - if let results = params.interfaceState.search?.resultsState { + if let externalSearchResultsCount = self.externalSearchResultsCount { + let value = presentationStringsFormattedNumber(externalSearchResultsCount, params.interfaceState.dateTimeFormat.groupingSeparator) + let suffix = params.interfaceState.strings.Chat_BottomSearchPanel_StoryCount(externalSearchResultsCount) + resultsTextString = [AnimatedTextComponent.Item( + id: "stories", + isUnbreakable: true, + content: .text(params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat(value, suffix).string) + )] + } else if let results = params.interfaceState.search?.resultsState { let displayTotalCount = results.completed ? results.messageIndices.count : Int(results.totalCount) if let currentId = results.currentId, let index = results.messageIndices.firstIndex(where: { $0.id == currentId }) { canChangeListMode = true @@ -237,7 +225,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { content: .text(params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat(value, suffix).string) )] } else if params.interfaceState.displayHistoryFilterAsList { - resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( + resultsTextString = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( ".", "." ), id: "total_count", mapping: [ @@ -247,7 +235,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { } else { let adjustedIndex = results.messageIndices.count - 1 - index - resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Items_NOfM( + resultsTextString = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Items_NOfM( ".", "." ), id: "position", mapping: [ @@ -263,7 +251,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { } else if let count = self.tagMessageCount?.count ?? self.totalMessageCount { canChangeListMode = count != 0 - resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( + resultsTextString = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( ".", "." ), id: "total_count", mapping: [ @@ -282,7 +270,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { } var modeButtonTitle: [AnimatedTextComponent.Item] = [] - modeButtonTitle = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeFormat("."), id: "mode", mapping: [ + modeButtonTitle = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeFormat("."), id: "mode", mapping: [ 0: params.interfaceState.displayHistoryFilterAsList ? .text(params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeChat) : .text(params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeList) ]) @@ -346,7 +334,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { var nextLeftX: CGFloat = 16.0 - if !self.alwaysShowTotalMessagesCount { + if !self.alwaysShowTotalMessagesCount && self.externalSearchResultsCount == nil { nextLeftX = 12.0 let calendarButtonSize = self.calendarButton.update( transition: .immediate, @@ -372,12 +360,28 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { if let calendarButtonView = self.calendarButton.view { if calendarButtonView.superview == nil { self.view.addSubview(calendarButtonView) + + if !transition.animation.isImmediate { + calendarButtonView.alpha = 1.0 + transition.animateAlpha(view: calendarButtonView, from: 0.0, to: 1.0) + transition.animateScale(view: calendarButtonView, from: 0.01, to: 1.0) + } } transition.setFrame(view: calendarButtonView, frame: calendarButtonFrame) } nextLeftX += calendarButtonSize.width + 8.0 } else if let calendarButtonView = self.calendarButton.view { - calendarButtonView.removeFromSuperview() + if transition.animation.isImmediate { + calendarButtonView.removeFromSuperview() + } else { + transition.setAlpha(view: calendarButtonView, alpha: 0.0, completion: { finished in + if finished { + calendarButtonView.removeFromSuperview() + } + calendarButtonView.alpha = 1.0 + }) + transition.animateScale(view: calendarButtonView, from: 1.0, to: 0.01) + } } if displaySearchMembers { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 76ef4d9905..ab8f61436c 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1924,8 +1924,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return inputPanelNode } - public func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController { - return HashtagSearchController(context: context, peer: peer, query: query, mode: all ? .noChat : .generic) + public func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController { + return HashtagSearchController(context: context, peer: peer, query: query, mode: stories ? .chatOnly : .generic, stories: stories, forceDark: forceDark) } public func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController { diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index 398da5383a..35f2fdfd62 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -9,23 +9,27 @@ public struct ChatTranslationState: Codable { enum CodingKeys: String, CodingKey { case baseLang case fromLang + case timestamp case toLang case isEnabled } public let baseLang: String public let fromLang: String + public let timestamp: Int32? public let toLang: String? public let isEnabled: Bool public init( baseLang: String, fromLang: String, + timestamp: Int32?, toLang: String?, isEnabled: Bool ) { self.baseLang = baseLang self.fromLang = fromLang + self.timestamp = timestamp self.toLang = toLang self.isEnabled = isEnabled } @@ -35,6 +39,7 @@ public struct ChatTranslationState: Codable { self.baseLang = try container.decode(String.self, forKey: .baseLang) self.fromLang = try container.decode(String.self, forKey: .fromLang) + self.timestamp = try container.decodeIfPresent(Int32.self, forKey: .timestamp) self.toLang = try container.decodeIfPresent(String.self, forKey: .toLang) self.isEnabled = try container.decode(Bool.self, forKey: .isEnabled) } @@ -44,6 +49,7 @@ public struct ChatTranslationState: Codable { try container.encode(self.baseLang, forKey: .baseLang) try container.encode(self.fromLang, forKey: .fromLang) + try container.encodeIfPresent(self.timestamp, forKey: .timestamp) try container.encodeIfPresent(self.toLang, forKey: .toLang) try container.encode(self.isEnabled, forKey: .isEnabled) } @@ -52,6 +58,7 @@ public struct ChatTranslationState: Codable { return ChatTranslationState( baseLang: self.baseLang, fromLang: self.fromLang, + timestamp: self.timestamp, toLang: toLang, isEnabled: self.isEnabled ) @@ -61,6 +68,7 @@ public struct ChatTranslationState: Codable { return ChatTranslationState( baseLang: self.baseLang, fromLang: self.fromLang, + timestamp: self.timestamp, toLang: self.toLang, isEnabled: isEnabled ) @@ -191,7 +199,8 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) return cachedChatTranslationState(engine: context.engine, peerId: peerId) |> mapToSignal { cached in - if let cached, cached.baseLang == baseLang { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if let cached, let timestamp = cached.timestamp, cached.baseLang == baseLang && currentTime - timestamp < 60 * 60 { if !dontTranslateLanguages.contains(cached.fromLang) { return .single(cached) } else { @@ -277,7 +286,13 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) if loggingEnabled { Logger.shared.log("ChatTranslation", "Ended with: \(fromLang)") } - let state = ChatTranslationState(baseLang: baseLang, fromLang: fromLang, toLang: nil, isEnabled: false) + let state = ChatTranslationState( + baseLang: baseLang, + fromLang: fromLang, + timestamp: currentTime, + toLang: cached?.toLang, + isEnabled: cached?.isEnabled ?? false + ) let _ = updateChatTranslationState(engine: context.engine, peerId: peerId, state: state).start() if !dontTranslateLanguages.contains(fromLang) { return state diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index cd9ab2fd67..c076ad3504 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -587,6 +587,8 @@ public final class WebAppController: ViewController, AttachmentContainable { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } } + + private var updateWebViewWhenStable = false func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { let previousLayout = self.validLayout?.0 @@ -605,6 +607,9 @@ public final class WebAppController: ViewController, AttachmentContainable { } let frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight - frameBottomInset))) + if !webView.frame.width.isZero && webView.frame != frame { + self.updateWebViewWhenStable = true + } var bottomInset = layout.intrinsicInsets.bottom + layout.additionalInsets.bottom if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 { @@ -635,6 +640,10 @@ public final class WebAppController: ViewController, AttachmentContainable { if let controller = self.controller { webView.updateMetrics(height: viewportFrame.height, isExpanded: controller.isContainerExpanded(), isStable: !controller.isContainerPanning(), transition: transition) + if self.updateWebViewWhenStable && !controller.isContainerPanning() { + self.updateWebViewWhenStable = false + webView.setNeedsLayout() + } } if layout.intrinsicInsets.bottom > 44.0 {