From 109fa6b5378470625144a916cdd34e61c88f108d Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 20 May 2025 17:25:39 +0800 Subject: [PATCH] [Temp] --- Telegram/BUILD | 4 +- build-system/bazel-utils/spm.bzl | 9 +- build-system/decrypt.rb | 5 + .../Sources/AccountContext.swift | 1 + .../Sources/Node/ChatListNodeLocation.swift | 20 +- .../Sources/Node/ChatListViewTransition.swift | 2 +- submodules/Display/Source/ListView.swift | 14 +- .../Display/Source/ViewController.swift | 4 +- .../Source/ObjCRuntimeUtils/RuntimeUtils.m | 1 + .../Sources/ReplaceBoostScreen.swift | 2 +- .../Sources/TelegramBaseController.swift | 5 +- ...ChatInlineSearchResultsListComponent.swift | 201 ++++++- .../Sources/ChatSideTopicsPanel.swift | 253 ++++----- .../Chat/ChatControllerLoadDisplayNode.swift | 3 +- .../TelegramUI/Sources/ChatController.swift | 8 +- .../Sources/ChatControllerNode.swift | 228 ++++++-- ...hatControllerTitlePanelNodeContainer.swift | 6 + .../ChatInterfaceTitlePanelNodes.swift | 39 +- .../ChatPinnedMessageTitlePanelNode.swift | 537 +++++++++--------- ...ChatTopicListTitleAccessoryPanelNode.swift | 229 ++++---- .../ContactMultiselectionController.swift | 4 +- .../Sources/SharedAccountContext.swift | 7 + .../TranslateUI/Sources/TranslateScreen.swift | 2 +- 23 files changed, 962 insertions(+), 622 deletions(-) diff --git a/Telegram/BUILD b/Telegram/BUILD index c40fa37fe1..6ed4339a42 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1785,8 +1785,10 @@ ios_application( #"//submodules/TelegramApi", #"//submodules/TelegramCore", #"//submodules/FFMpegBinding", - "//submodules/Display", + #"//submodules/Display", #"//third-party/webrtc", + "//submodules/AsyncDisplayKit", + "//submodules/ObjCRuntimeUtils", ], ) diff --git a/build-system/bazel-utils/spm.bzl b/build-system/bazel-utils/spm.bzl index 793d804c2a..7cd7038a13 100644 --- a/build-system/bazel-utils/spm.bzl +++ b/build-system/bazel-utils/spm.bzl @@ -95,6 +95,7 @@ _IGNORE_OBJC_LIBRARY_ATTRS = [ "toolchains", "transitive_configs", "visibility", + "package_metadata", ] _IGNORE_OBJC_LIBRARY_EMPTY_ATTRS = [ @@ -106,6 +107,8 @@ _IGNORE_OBJC_LIBRARY_EMPTY_ATTRS = [ "restricted_to", "textual_hdrs", "sdk_includes", + "conlyopts", + "cxxopts", ] _OBJC_LIBRARY_ATTRS = { @@ -154,6 +157,8 @@ _IGNORE_SWIFT_LIBRARY_ATTRS = [ "toolchains", "transitive_configs", "visibility", + "library_evolution", + "package_metadata", ] _IGNORE_SWIFT_LIBRARY_EMPTY_ATTRS = [ @@ -332,9 +337,9 @@ def _collect_spm_modules_impl(target, ctx): # Extract the path from the label # Example: @//path/ModuleName:ModuleSubname -> path/ModuleName - if not str(ctx.label).startswith("@//"): + if not str(ctx.label).startswith("@@//"): fail("Invalid label: {}".format(ctx.label)) - module_path = str(ctx.label).split(":")[0].split("@//")[1] + module_path = str(ctx.label).split(":")[0].split("@@//")[1] if module_type == "objc_library": module_info = { diff --git a/build-system/decrypt.rb b/build-system/decrypt.rb index ba126e9db3..5f89736a25 100644 --- a/build-system/decrypt.rb +++ b/build-system/decrypt.rb @@ -51,6 +51,11 @@ class EncryptionV2 key = keyIv[0..31] iv = keyIv[32..43] auth_data = keyIv[44..-1] + + puts "key: #{key.inspect}" + puts "iv: #{iv.inspect}" + puts "auth_data: #{auth_data.inspect}" + cipher.key = key cipher.iv = iv cipher.auth_data = auth_data diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index cfaa4dcb9b..66e7c09b92 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1073,6 +1073,7 @@ public protocol SharedAccountContext: AnyObject { selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode ) -> ChatHistoryListNode + func subscribeChatListData(context: AccountContext, location: ChatListControllerLocation) -> Signal func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: ((UIView?, CGPoint?) -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makeChatMessageAvatarHeaderItem(context: AccountContext, timestamp: Int32, peer: Peer, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift index dea1d3d354..c999b87ee9 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift @@ -6,12 +6,12 @@ import Display import TelegramUIPreferences import AccountContext -enum ChatListNodeLocation: Equatable { +public enum ChatListNodeLocation: Equatable { case initial(count: Int, filter: ChatListFilter?) case navigation(index: EngineChatList.Item.Index, filter: ChatListFilter?) case scroll(index: EngineChatList.Item.Index, sourceIndex: EngineChatList.Item.Index, scrollPosition: ListViewScrollPosition, animated: Bool, filter: ChatListFilter?) - var filter: ChatListFilter? { + public var filter: ChatListFilter? { switch self { case let .initial(_, filter): return filter @@ -23,10 +23,16 @@ enum ChatListNodeLocation: Equatable { } } -struct ChatListNodeViewUpdate { - let list: EngineChatList - let type: ViewUpdateType - let scrollPosition: ChatListNodeViewScrollPosition? +public struct ChatListNodeViewUpdate { + public let list: EngineChatList + public let type: ViewUpdateType + public let scrollPosition: ChatListNodeViewScrollPosition? + + public init(list: EngineChatList, type: ViewUpdateType, scrollPosition: ChatListNodeViewScrollPosition?) { + self.list = list + self.type = type + self.scrollPosition = scrollPosition + } } public func chatListFilterPredicate(filter: ChatListFilterData, accountPeerId: EnginePeer.Id) -> ChatListFilterPredicate { @@ -113,7 +119,7 @@ public func chatListFilterPredicate(filter: ChatListFilterData, accountPeerId: E }) } -func chatListViewForLocation(chatListLocation: ChatListControllerLocation, location: ChatListNodeLocation, account: Account, shouldLoadCanMessagePeer: Bool) -> Signal { +public func chatListViewForLocation(chatListLocation: ChatListControllerLocation, location: ChatListNodeLocation, account: Account, shouldLoadCanMessagePeer: Bool) -> Signal { let accountPeerId = account.peerId switch chatListLocation { diff --git a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift index 5d10a7a9ed..630edbf917 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift @@ -47,7 +47,7 @@ struct ChatListNodeViewTransition { let animateCrossfade: Bool } -enum ChatListNodeViewScrollPosition { +public enum ChatListNodeViewScrollPosition { case index(index: ChatListIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) } diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 581b4b0238..f88e3ded10 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -2376,16 +2376,24 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel } else { let updateItem = updateIndicesAndItems[0] if let previousNode = previousNodes[updateItem.index] { + let threadId = pthread_self() + var tailRecurse = false self.nodeForItem(synchronous: synchronous, synchronousLoads: synchronousLoads, item: updateItem.item, previousNode: previousNode, index: updateItem.index, previousItem: updateItem.index == 0 ? nil : self.items[updateItem.index - 1], nextItem: updateItem.index == (self.items.count - 1) ? nil : self.items[updateItem.index + 1], params: ListViewItemLayoutParams(width: state.visibleSize.width, leftInset: state.insets.left, rightInset: state.insets.right, availableHeight: state.visibleSize.height - state.insets.top - state.insets.bottom), updateAnimationIsAnimated: animated, updateAnimationIsCrossfade: crossfade, customAnimationTransition: customAnimationTransition, completion: { _, layout, apply in state.updateNodeAtItemIndex(updateItem.index, layout: layout, direction: updateItem.directionHint, isAnimated: animated, apply: apply, operations: &operations) updateIndicesAndItems.remove(at: 0) - self.updateNodes(synchronous: synchronous, synchronousLoads: synchronousLoads, crossfade: crossfade, customAnimationTransition: customAnimationTransition, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion) + if pthread_equal(pthread_self(), threadId) != 0 && !tailRecurse { + tailRecurse = true + } else { + self.updateNodes(synchronous: synchronous, synchronousLoads: synchronousLoads, crossfade: crossfade, customAnimationTransition: customAnimationTransition, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion) + } }) - break + if !tailRecurse { + tailRecurse = true + break + } } else { updateIndicesAndItems.remove(at: 0) - //self.updateNodes(synchronous: synchronous, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion) } } } diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 90c53ba32c..b506acf018 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -424,10 +424,10 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { } open func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: 0.0, transition: transition) + self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: 0.0, additionalCutout: nil, transition: transition) } - public func applyNavigationBarLayout(_ layout: ContainerViewLayout, navigationLayout: NavigationLayout, additionalBackgroundHeight: CGFloat, transition: ContainedViewLayoutTransition) { + public func applyNavigationBarLayout(_ layout: ContainerViewLayout, navigationLayout: NavigationLayout, additionalBackgroundHeight: CGFloat, additionalCutout: CGSize?, transition: ContainedViewLayoutTransition) { let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: navigationLayout.navigationFrame.maxY)) diff --git a/submodules/ObjCRuntimeUtils/Source/ObjCRuntimeUtils/RuntimeUtils.m b/submodules/ObjCRuntimeUtils/Source/ObjCRuntimeUtils/RuntimeUtils.m index 4435e0b5dd..82b42731c5 100644 --- a/submodules/ObjCRuntimeUtils/Source/ObjCRuntimeUtils/RuntimeUtils.m +++ b/submodules/ObjCRuntimeUtils/Source/ObjCRuntimeUtils/RuntimeUtils.m @@ -1,4 +1,5 @@ #import "RuntimeUtils.h" +#include #import diff --git a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift index 1aefe3e225..48d031b145 100644 --- a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift +++ b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift @@ -977,7 +977,7 @@ public class ReplaceBoostScreen: ViewController { layout.statusBarHeight = nil - self.applyNavigationBarLayout(layout, navigationLayout: navigationLayout, additionalBackgroundHeight: 0.0, transition: transition) + self.applyNavigationBarLayout(layout, navigationLayout: navigationLayout, additionalBackgroundHeight: 0.0, additionalCutout: nil, transition: transition) } override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 7872e0a3cc..a3bab5ae2b 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -394,13 +394,14 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { private var suspendNavigationBarLayout: Bool = false private var suspendedNavigationBarLayout: ContainerViewLayout? private var additionalNavigationBarBackgroundHeight: CGFloat = 0.0 + private var additionalNavigationBarCutout: CGSize? override open func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { if self.suspendNavigationBarLayout { self.suspendedNavigationBarLayout = layout return } - self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, additionalCutout: self.additionalNavigationBarCutout, transition: transition) } override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -969,7 +970,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { self.suspendNavigationBarLayout = false if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout { self.suspendedNavigationBarLayout = suspendedNavigationBarLayout - self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, additionalNavigationBarCutout: self.additionalNavigationBarCutout, transition: transition) } self.accessoryPanelContainerHeight = additionalHeight diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 105ce3a39c..24bc6ed9ed 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -71,6 +71,7 @@ public final class ChatInlineSearchResultsListComponent: Component { case empty case tag(MemoryBuffer) case search(query: String, includeSavedPeers: Bool) + case monoforumChats(query: String) } public let context: AccountContext @@ -85,6 +86,7 @@ public final class ChatInlineSearchResultsListComponent: Component { public let loadTagMessages: (MemoryBuffer, MessageIndex?) -> Signal? public let getSearchResult: () -> Signal? public let getSavedPeers: (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>? + public let getChats: (String) -> Signal? public let loadMoreSearchResults: () -> Void public init( @@ -100,6 +102,7 @@ public final class ChatInlineSearchResultsListComponent: Component { loadTagMessages: @escaping (MemoryBuffer, MessageIndex?) -> Signal?, getSearchResult: @escaping () -> Signal?, getSavedPeers: @escaping (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>?, + getChats: @escaping (String) -> Signal?, loadMoreSearchResults: @escaping () -> Void ) { self.context = context @@ -114,6 +117,7 @@ public final class ChatInlineSearchResultsListComponent: Component { self.loadTagMessages = loadTagMessages self.getSearchResult = getSearchResult self.getSavedPeers = getSavedPeers + self.getChats = getChats self.loadMoreSearchResults = loadMoreSearchResults } @@ -146,10 +150,12 @@ public final class ChatInlineSearchResultsListComponent: Component { enum Id: Hashable { case peer(EnginePeer.Id) case message(EngineMessage.Id) + case chat(EngineChatList.Item.Id) } case peer(EnginePeer) case message(EngineMessage) + case chat(EngineChatList.Item) var id: Id { switch self { @@ -157,6 +163,8 @@ public final class ChatInlineSearchResultsListComponent: Component { return .peer(peer.id) case let .message(message): return .message(message.id) + case let .chat(chat): + return .chat(chat.id) } } @@ -174,6 +182,12 @@ public final class ChatInlineSearchResultsListComponent: Component { } else { return false } + case let .chat(chat): + if case .chat(chat) = rhs { + return true + } else { + return false + } } } @@ -188,14 +202,27 @@ public final class ChatInlineSearchResultsListComponent: Component { return lhsPeer.id < rhsPeer.id case .message: return true + case .chat: + return true } case let .message(lhsMessage): switch rhs { case .peer: return false + case .chat: + return false case let .message(rhsMessage): return lhsMessage.index > rhsMessage.index } + case let .chat(lhsChat): + switch rhs { + case let .chat(rhsChat): + return lhsChat.index > rhsChat.index + case .peer: + return false + case .message: + return true + } } } } @@ -418,6 +445,8 @@ public final class ChatInlineSearchResultsListComponent: Component { } case .search: break + case .monoforumChats: + break } } } else if let (currentIndex, disposable) = self.searchContents { @@ -431,6 +460,8 @@ public final class ChatInlineSearchResultsListComponent: Component { self.searchContents = (loadAroundIndex, disposable) component.loadMoreSearchResults() + case .monoforumChats: + break } } } @@ -581,6 +612,127 @@ public final class ChatInlineSearchResultsListComponent: Component { })) } } + case let .monoforumChats(query): + let _ = query + + if previousComponent?.contents != component.contents { + self.tagContents?.disposable?.dispose() + self.tagContents = nil + + self.searchContents?.disposable?.dispose() + self.searchContents = nil + + let disposable = MetaDisposable() + self.searchContents = (nil, disposable) + + let savedPeers: Signal + if let savedPeersSignal = component.getChats(query) { + savedPeers = savedPeersSignal + } else { + savedPeers = .single(nil) + } + + disposable.set((savedPeers + |> deliverOnMainQueue).startStrict(next: { [weak self] chatList in + guard let self else { + return + } + + let messages: [EngineMessage] = [] /*result?.messages.map { entry in + return EngineMessage(entry) + } ?? []*/ + + var entries: [Entry] = [] + if let chatList { + for item in chatList.items { + entries.append(.chat(item)) + } + } + for message in messages { + entries.append(.message(message)) + } + entries.sort() + + let contentsId = self.nextContentsId + self.nextContentsId += 1 + self.contentsState = ContentsState( + id: contentsId, + contentId: .search(query), + entries: entries, + messages: messages, + hasEarlier: false, //!(result?.completed ?? true), + hasLater: false + ) + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + + if !self.didSetReady { + self.didSetReady = true + self.isReadyPromise.set(.single(true)) + } + })) + + /*if !query.isEmpty, let savedPeersSignal = component.getSavedPeers(query) { + savedPeers = savedPeersSignal + } else { + savedPeers = .single([]) + }*/ + + /*if let historySignal = component.getSearchResult() { + disposable.set((savedPeers + |> mapToSignal { savedPeers -> Signal<([(EnginePeer, MessageIndex?)], SearchMessagesResult?), NoError> in + if savedPeers.isEmpty { + return historySignal + |> map { result in + return ([], result) + } + } else { + return (.single(nil) |> then(historySignal)) + |> map { result in + return (savedPeers, result) + } + } + } + |> deliverOnMainQueue).startStrict(next: { [weak self] savedPeers, result in + guard let self else { + return + } + + let messages: [EngineMessage] = result?.messages.map { entry in + return EngineMessage(entry) + } ?? [] + + var entries: [Entry] = [] + for (peer, _) in savedPeers { + entries.append(.peer(peer)) + } + for message in messages { + entries.append(.message(message)) + } + entries.sort() + + let contentsId = self.nextContentsId + self.nextContentsId += 1 + self.contentsState = ContentsState( + id: contentsId, + contentId: .search(query), + entries: entries, + messages: messages, + hasEarlier: !(result?.completed ?? true), + hasLater: false + ) + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + + if !self.didSetReady { + self.didSetReady = true + self.isReadyPromise.set(.single(true)) + } + })) + }*/ + } } if let contentsState = self.contentsState, self.contentsState != self.appliedContentsState { @@ -607,13 +759,17 @@ public final class ChatInlineSearchResultsListComponent: Component { }, additionalCategorySelected: { _ in }, - messageSelected: { [weak self] _, _, message, _ in - guard let self else { + messageSelected: { [weak self] peer, _, message, _ in + guard let self, let component = self.component else { return } self.listNode.clearHighlightAnimated(true) - self.component?.messageSelected(message) + if case .monoforumChats = component.contents { + component.peerSelected(peer) + } else { + component.messageSelected(message) + } }, groupSelected: { _ in }, @@ -845,6 +1001,45 @@ public final class ChatInlineSearchResultsListComponent: Component { hiddenOffset: false, interaction: chatListNodeInteraction ) + case let .chat(item): + return ChatListItem( + presentationData: chatListPresentationData, + context: component.context, + chatListLocation: component.peerId.flatMap { peerId in .savedMessagesChats(peerId: peerId) } ?? .chatList(groupId: .root), + filterData: nil, + index: item.index, + content: .peer(ChatListItemContent.PeerData( + messages: item.messages, + peer: item.renderedPeer, + threadInfo: nil, + combinedReadState: nil, + isRemovedFromTotalUnreadCount: false, + presence: nil, + hasUnseenMentions: false, + hasUnseenReactions: false, + draftState: nil, + mediaDraftContentType: nil, + inputActivities: nil, + promoInfo: nil, + ignoreUnreadBadge: false, + displayAsMessage: component.peerId != component.context.account.peerId && !component.showEmptyResults, + hasFailedMessages: false, + forumTopicData: nil, + topForumTopicItems: [], + autoremoveTimeout: nil, + storyState: nil, + requiresPremiumForMessaging: false, + displayAsTopicList: false, + tags: [] + )), + editing: false, + hasActiveRevealControls: false, + selected: false, + header: nil, + enableContextActions: false, + hiddenOffset: false, + interaction: chatListNodeInteraction + ) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift index 0e05b95c73..513987bd16 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift @@ -37,6 +37,7 @@ public final class ChatSideTopicsPanel: Component { let theme: PresentationTheme let strings: PresentationStrings let peerId: EnginePeer.Id + let isMonoforum: Bool let topicId: Int64? let togglePanel: () -> Void let updateTopicId: (Int64?) -> Void @@ -46,6 +47,7 @@ public final class ChatSideTopicsPanel: Component { theme: PresentationTheme, strings: PresentationStrings, peerId: EnginePeer.Id, + isMonoforum: Bool, topicId: Int64?, togglePanel: @escaping () -> Void, updateTopicId: @escaping (Int64?) -> Void @@ -54,6 +56,7 @@ public final class ChatSideTopicsPanel: Component { self.theme = theme self.strings = strings self.peerId = peerId + self.isMonoforum = isMonoforum self.topicId = topicId self.togglePanel = togglePanel self.updateTopicId = updateTopicId @@ -72,6 +75,9 @@ public final class ChatSideTopicsPanel: Component { if lhs.peerId != rhs.peerId { return false } + if lhs.isMonoforum != rhs.isMonoforum { + return false + } if lhs.topicId != rhs.topicId { return false } @@ -111,7 +117,7 @@ public final class ChatSideTopicsPanel: Component { private let containerButton: HighlightTrackingButton - private let icon = ComponentView() + private var icon: ComponentView? private var avatarNode: AvatarNode? private let title = ComponentView() @@ -179,34 +185,78 @@ public final class ChatSideTopicsPanel: Component { func update(context: AccountContext, item: Item, isSelected: Bool, theme: PresentationTheme, width: CGFloat, transition: ComponentTransition) -> CGSize { let spacing: CGFloat = 3.0 + let iconSize = CGSize(width: 30.0, height: 30.0) - let avatarIconContent: EmojiStatusComponent.Content - if case let .forum(topicId) = item.item.id, topicId != 1, let threadData = item.item.threadData { - if let fileId = threadData.info.icon, fileId != 0 { - avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(0)) + var avatarIconContent: EmojiStatusComponent.Content? + if case let .forum(topicId) = item.item.id { + if topicId != 1, let threadData = item.item.threadData { + if let fileId = threadData.info.icon, fileId != 0 { + avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: iconSize, placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(0)) + } else { + avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: iconSize) + } } else { - avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: CGSize(width: 18.0, height: 18.0)) + avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicIcon(theme)) } - } else { - avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicIcon(theme)) } - let avatarIconComponent = EmojiStatusComponent( - context: context, - animationCache: context.animationCache, - animationRenderer: context.animationRenderer, - content: avatarIconContent, - isVisibleForAnimations: false, - action: nil - ) - let iconSize = self.icon.update( - transition: .immediate, - component: AnyComponent(avatarIconComponent), - environment: {}, - containerSize: CGSize(width: 30.0, height: 30.0) - ) + if let avatarIconContent { + let avatarIconComponent = EmojiStatusComponent( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + content: avatarIconContent, + isVisibleForAnimations: false, + action: nil + ) + let icon: ComponentView + if let current = self.icon { + icon = current + } else { + icon = ComponentView() + self.icon = icon + } + let _ = icon.update( + transition: .immediate, + component: AnyComponent(avatarIconComponent), + environment: {}, + containerSize: iconSize + ) + } else if let icon = self.icon { + self.icon = nil + icon.view?.removeFromSuperview() + } + + let titleText: String + if case let .forum(topicId) = item.item.id { + let _ = topicId + if let threadData = item.item.threadData { + titleText = threadData.info.title + } else { + //TODO:localize + titleText = "General" + } + } else { + titleText = item.item.renderedPeer.chatMainPeer?.compactDisplayTitle ?? " " + } + + if let avatarIconContent, let icon = self.icon { + let avatarIconComponent = EmojiStatusComponent( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + content: avatarIconContent, + isVisibleForAnimations: false, + action: nil + ) + let _ = icon.update( + transition: .immediate, + component: AnyComponent(avatarIconComponent), + environment: {}, + containerSize: iconSize + ) + } - let titleText: String = item.item.renderedPeer.chatMainPeer?.compactDisplayTitle ?? " " let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -224,39 +274,38 @@ public final class ChatSideTopicsPanel: Component { let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: 0.0), size: iconSize) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: iconFrame.maxY + spacing), size: titleSize) - if let iconView = self.icon.view { - if iconView.superview == nil { - iconView.isUserInteractionEnabled = false - self.containerButton.addSubview(iconView) - } - iconView.frame = iconFrame - - if "".isEmpty { - iconView.isHidden = true - - let avatarNode: AvatarNode - if let current = self.avatarNode { - avatarNode = current - } else { - avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 11.0)) - avatarNode.isUserInteractionEnabled = false - self.avatarNode = avatarNode - self.containerButton.addSubview(avatarNode.view) - } - avatarNode.frame = iconFrame - avatarNode.updateSize(size: iconFrame.size) - - if let peer = item.item.renderedPeer.chatMainPeer { - if peer.smallProfileImage != nil { - avatarNode.setPeerV2(context: context, theme: theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) - } else { - avatarNode.setPeer(context: context, theme: theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) - } - } - } else if let avatarNode = self.avatarNode { + if let icon = self.icon { + if let avatarNode = self.avatarNode { self.avatarNode = nil avatarNode.view.removeFromSuperview() - iconView.isHidden = false + } + + if let iconView = icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.containerButton.addSubview(iconView) + } + iconView.frame = iconFrame + } + } else { + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 11.0)) + avatarNode.isUserInteractionEnabled = false + self.avatarNode = avatarNode + self.containerButton.addSubview(avatarNode.view) + } + avatarNode.frame = iconFrame + avatarNode.updateSize(size: iconFrame.size) + + if let peer = item.item.renderedPeer.chatMainPeer { + if peer.smallProfileImage != nil { + avatarNode.setPeerV2(context: context, theme: theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) + } else { + avatarNode.setPeer(context: context, theme: theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) + } } } @@ -600,7 +649,15 @@ public final class ChatSideTopicsPanel: Component { public func topicIndex(threadId: Int64?) -> Int? { if let threadId { - if let value = self.items.firstIndex(where: { $0.id == .chatList(PeerId(threadId)) }) { + if let value = self.items.firstIndex(where: { item in + if item.id == .chatList(PeerId(threadId)) { + return true + } else if item.id == .forum(threadId) { + return true + } else { + return false + } + }) { return value + 1 } else { return nil @@ -619,77 +676,7 @@ public final class ChatSideTopicsPanel: Component { self.state = state if self.component == nil { - let viewKey: PostboxViewKey = .savedMessagesIndex(peerId: component.peerId) - let interfaceStateKey: PostboxViewKey = .chatInterfaceState(peerId: component.peerId) - - let accountPeerId = component.context.account.peerId - let threadListSignal: Signal = component.context.account.postbox.combinedView(keys: [viewKey, interfaceStateKey]) - |> map { views -> EngineChatList in - guard let view = views.views[viewKey] as? MessageHistorySavedMessagesIndexView else { - preconditionFailure() - } - - var draft: EngineChatList.Draft? - if let interfaceStateView = views.views[interfaceStateKey] as? ChatInterfaceStateView { - if let embeddedState = interfaceStateView.value, let _ = embeddedState.overrideChatTimestamp { - if let opaqueState = _internal_decodeStoredChatInterfaceState(state: embeddedState) { - if let text = opaqueState.synchronizeableInputState?.text { - draft = EngineChatList.Draft(text: text, entities: opaqueState.synchronizeableInputState?.entities ?? []) - } - } - } - } - - var items: [EngineChatList.Item] = [] - for item in view.items { - guard let sourcePeer = item.peer else { - continue - } - - let sourceId = PeerId(item.id) - - var messages: [EngineMessage] = [] - if let topMessage = item.topMessage { - messages.append(EngineMessage(topMessage)) - } - - let mappedMessageIndex = MessageIndex(id: MessageId(peerId: sourceId, namespace: item.index.id.namespace, id: item.index.id.id), timestamp: item.index.timestamp) - - items.append(EngineChatList.Item( - id: .chatList(sourceId), - index: .chatList(ChatListIndex(pinningIndex: item.pinnedIndex.flatMap(UInt16.init), messageIndex: mappedMessageIndex)), - messages: messages, - readCounters: nil, - isMuted: false, - draft: sourceId == accountPeerId ? draft : nil, - threadData: nil, - renderedPeer: EngineRenderedPeer(peer: EnginePeer(sourcePeer)), - presence: nil, - hasUnseenMentions: false, - hasUnseenReactions: false, - forumTopicData: nil, - topForumTopicItems: [], - hasFailed: false, - isContact: false, - autoremoveTimeout: nil, - storyStats: nil, - displayAsTopicList: false, - isPremiumRequiredToMessage: false, - mediaDraftContentType: nil - )) - } - - let list = EngineChatList( - items: items.reversed(), - groupItems: [], - additionalItems: [], - hasEarlier: false, - hasLater: false, - isLoading: view.isLoading - ) - - return list - } + let threadListSignal: Signal = component.context.sharedContext.subscribeChatListData(context: component.context, location: component.isMonoforum ? .savedMessagesChats(peerId: component.peerId) : .forum(peerId: component.peerId)) self.itemsDisposable = (threadListSignal |> deliverOnMainQueue).startStrict(next: { [weak self] chatList in @@ -864,8 +851,12 @@ public final class ChatSideTopicsPanel: Component { guard let self, let component = self.component else { return } - let topicId = chatListItem.renderedPeer.peerId.toInt64() - component.updateTopicId(topicId) + if case let .forum(topicId) = item.item.id { + component.updateTopicId(topicId) + } else { + let topicId = chatListItem.renderedPeer.peerId.toInt64() + component.updateTopicId(topicId) + } }, contextGesture: { gesture, sourceNode in }) self.itemViews[itemId] = itemView @@ -873,8 +864,10 @@ public final class ChatSideTopicsPanel: Component { } var isSelected = false - if component.topicId == item.item.renderedPeer.peerId.toInt64() { - isSelected = true + if case let .forum(topicId) = item.item.id { + isSelected = component.topicId == topicId + } else { + isSelected = component.topicId == item.item.renderedPeer.peerId.toInt64() } let itemSize = itemView.update(context: component.context, item: item, isSelected: isSelected, theme: component.theme, width: panelWidth, transition: .immediate) let itemFrame = CGRect(origin: CGPoint(x: containerInsets.left, y: contentSize.height), size: itemSize) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 6d5bdbbedc..3b29c5e51f 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -2729,9 +2729,10 @@ extension ChatControllerImpl { } mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, scrolledToSomeIndex: transition.scrolledToSomeIndex, peerType: transition.peerType, networkType: transition.networkType, animateIn: false, reason: transition.reason, flashIndicators: transition.flashIndicators, animateFromPreviousFilter: false), updateSizeAndInsets) - }, updateExtraNavigationBarBackgroundHeight: { value, hitTestSlop, _ in + }, updateExtraNavigationBarBackgroundHeight: { value, hitTestSlop, cutout, _ in strongSelf.additionalNavigationBarBackgroundHeight = value strongSelf.additionalNavigationBarHitTestSlop = hitTestSlop + strongSelf.additionalNavigationBarCutout = cutout }) if let mappedTransition = mappedTransition { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 7a4be77234..5c428e26db 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -7072,13 +7072,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var suspendedNavigationBarLayout: ContainerViewLayout? var additionalNavigationBarBackgroundHeight: CGFloat = 0.0 var additionalNavigationBarHitTestSlop: CGFloat = 0.0 + var additionalNavigationBarCutout: CGSize? override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { if self.suspendNavigationBarLayout { self.suspendedNavigationBarLayout = layout return } - self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, additionalCutout: self.additionalNavigationBarCutout, transition: transition) } override public func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { @@ -7116,10 +7117,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var navigationBarTransition = transition self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop, completion in self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop, completion: completion) - }, updateExtraNavigationBarBackgroundHeight: { value, hitTestSlop, extraNavigationTransition in + }, updateExtraNavigationBarBackgroundHeight: { value, hitTestSlop, cutout, extraNavigationTransition in navigationBarTransition = extraNavigationTransition self.additionalNavigationBarBackgroundHeight = value self.additionalNavigationBarHitTestSlop = hitTestSlop + self.additionalNavigationBarCutout = cutout }) if case .compact = layout.metrics.widthClass { @@ -7138,7 +7140,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.suspendNavigationBarLayout = false if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout { self.suspendedNavigationBarLayout = suspendedNavigationBarLayout - self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: navigationBarTransition) + self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, additionalCutout: self.additionalNavigationBarCutout, transition: navigationBarTransition) } self.navigationBar?.additionalContentNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: 0.0, bottom: self.additionalNavigationBarHitTestSlop, right: 0.0) } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 5cdf3810e6..952616d398 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -351,7 +351,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate, listViewTransaction: { _, _, _, _ in - }, updateExtraNavigationBarBackgroundHeight: { _, _, _ in + }, updateExtraNavigationBarBackgroundHeight: { _, _, _, _ in }) } } @@ -1033,7 +1033,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.wrappingNode.update(size: layout.size, cornerRadius: layout.deviceMetrics.screenCornerRadius, transition: .immediate) } - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition protoTransition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets, CGFloat, Bool, @escaping () -> Void) -> Void, updateExtraNavigationBarBackgroundHeight: (CGFloat, CGFloat, ContainedViewLayoutTransition) -> Void) { + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition protoTransition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets, CGFloat, Bool, @escaping () -> Void) -> Void, updateExtraNavigationBarBackgroundHeight: (CGFloat, CGFloat, CGSize?, ContainedViewLayoutTransition) -> Void) { let transition: ContainedViewLayoutTransition if let _ = self.scheduledAnimateInAsOverlayFromNode { transition = .immediate @@ -1302,6 +1302,28 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.titleTopicsAccessoryPanelNode = nil } + var defaultLeftPanelWidth: CGFloat = 72.0 + defaultLeftPanelWidth += layout.safeInsets.left + let leftPanelLeftInset = defaultLeftPanelWidth - 72.0 + + var leftPanelSize: CGSize? + var dismissedLeftPanel: (component: AnyComponentWithIdentity, view: ComponentView)? + var immediatelyLayoutLeftPanelNodeAndAnimateAppearance = false + if let leftPanelComponent = sidePanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.leftPanel?.component, controllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction, force: false) { + if self.leftPanel?.component.id != leftPanelComponent.id { + dismissedLeftPanel = self.leftPanel + self.leftPanel = (leftPanelComponent, ComponentView()) + immediatelyLayoutLeftPanelNodeAndAnimateAppearance = true + } else if let leftPanel = self.leftPanel { + self.leftPanel = (leftPanelComponent, leftPanel.view) + } + + leftPanelSize = CGSize(width: defaultLeftPanelWidth, height: layout.size.height) + } else if let leftPanel = self.leftPanel { + dismissedLeftPanel = leftPanel + self.leftPanel = nil + } + var dismissedTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode? var immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance = false var titleAccessoryPanelHeight: CGFloat? @@ -1321,7 +1343,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - let layoutResult = titleAccessoryPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + let layoutResult = titleAccessoryPanelNode.updateLayout(width: layout.size.width, leftInset: leftPanelSize?.width ?? layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) titleAccessoryPanelHeight = layoutResult.insetHeight titleAccessoryPanelBackgroundHeight = layoutResult.backgroundHeight titleAccessoryPanelHitTestSlop = layoutResult.hitTestSlop @@ -1489,30 +1511,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.feePanelNode = nil } - var defaultLeftPanelWidth: CGFloat = 72.0 - if case .landscapeRight = layout.metrics.orientation { - defaultLeftPanelWidth += layout.safeInsets.left - } - let leftPanelLeftInset = defaultLeftPanelWidth - 72.0 - - var leftPanelSize: CGSize? - var dismissedLeftPanel: (component: AnyComponentWithIdentity, view: ComponentView)? - var immediatelyLayoutLeftPanelNodeAndAnimateAppearance = false - if let leftPanelComponent = sidePanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.leftPanel?.component, controllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction, force: false) { - if self.leftPanel?.component.id != leftPanelComponent.id { - dismissedLeftPanel = self.leftPanel - self.leftPanel = (leftPanelComponent, ComponentView()) - immediatelyLayoutLeftPanelNodeAndAnimateAppearance = true - } else if let leftPanel = self.leftPanel { - self.leftPanel = (leftPanelComponent, leftPanel.view) - } - - leftPanelSize = CGSize(width: defaultLeftPanelWidth, height: layout.size.height) - } else if let leftPanel = self.leftPanel { - dismissedLeftPanel = leftPanel - self.leftPanel = nil - } - self.controllerInteraction.isSidePanelOpen = self.leftPanel != nil var inputPanelNodeBaseHeight: CGFloat = 0.0 @@ -1793,6 +1791,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.historyNode.isSelectionGestureEnabled = isSelectionEnabled transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 200.0))) + self.titleAccessoryPanelContainer.hitTestExcludeInsets = UIEdgeInsets(top: 0.0, left: leftPanelSize?.width ?? 0.0, bottom: 0.0, right: 0.0) transition.updateFrame(node: self.inputContextPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height))) transition.updateFrame(node: self.inputContextOverTextPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height))) @@ -1848,8 +1847,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { insets.top += panelHeight extraNavigationBarHeight += panelHeight } + + var extraNavigationBarLeftCutout: CGSize? + extraNavigationBarLeftCutout = CGSize(width: 44.0, height: 30.0) - updateExtraNavigationBarBackgroundHeight(extraNavigationBarHeight, extraNavigationBarHitTestSlop, extraTransition) + updateExtraNavigationBarBackgroundHeight(extraNavigationBarHeight, extraNavigationBarHitTestSlop, extraNavigationBarLeftCutout, extraTransition) let contentBounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width - wrappingInsets.left - wrappingInsets.right, height: layout.size.height - wrappingInsets.top - wrappingInsets.bottom) @@ -2270,8 +2272,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } if immediatelyLayoutLeftPanelNodeAndAnimateAppearance { leftPanelView.frame = leftPanelFrame.offsetBy(dx: -leftPanelSize.width, dy: 0.0) - if let leftPanelView = leftPanelView as? ChatSideTopicsPanel.View { - leftPanelView.updateGlobalOffset(globalOffset: -leftPanelSize.width, transition: ComponentTransition(transition)) + + if self.titleTopicsAccessoryPanelNode != nil { + if let leftPanelView = leftPanelView as? ChatSideTopicsPanel.View { + leftPanelView.updateGlobalOffset(globalOffset: -leftPanelSize.width, transition: ComponentTransition(transition)) + } } } transition.updateFrame(view: leftPanelView, frame: leftPanelFrame) @@ -2298,7 +2303,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { dismissedLeftPanelView?.removeFromSuperview() }) if let dismissedLeftPanelView = dismissedLeftPanelView as? ChatSideTopicsPanel.View { - dismissedLeftPanelView.updateGlobalOffset(globalOffset: -dismissedLeftPanelSize.width, transition: ComponentTransition(transition)) + if self.titleTopicsAccessoryPanelNode != nil { + dismissedLeftPanelView.updateGlobalOffset(globalOffset: -dismissedLeftPanelSize.width, transition: ComponentTransition(transition)) + } } } @@ -2352,7 +2359,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let titleTopicsAccessoryPanelNode = self.titleTopicsAccessoryPanelNode, let titleTopicsAccessoryPanelFrame, (immediatelyLayoutTitleTopicsAccessoryPanelNodeAndAnimateAppearance || !titleTopicsAccessoryPanelNode.frame.equalTo(titleTopicsAccessoryPanelFrame)) { if immediatelyLayoutTitleTopicsAccessoryPanelNodeAndAnimateAppearance { titleTopicsAccessoryPanelNode.frame = titleTopicsAccessoryPanelFrame.offsetBy(dx: 0.0, dy: -titleTopicsAccessoryPanelFrame.height) - titleTopicsAccessoryPanelNode.updateGlobalOffset(globalOffset: -titleTopicsAccessoryPanelFrame.height, transition: .immediate) + if self.leftPanel != nil { + titleTopicsAccessoryPanelNode.updateGlobalOffset(globalOffset: -titleTopicsAccessoryPanelFrame.height, transition: .immediate) + } let topPanelTransition = ComponentTransition(transition) /*switch topPanelTransition.animation { @@ -2378,8 +2387,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let previousFrame = titleAccessoryPanelNode.frame titleAccessoryPanelNode.frame = titleAccessoryPanelFrame if transition.isAnimated && previousFrame.width != titleAccessoryPanelFrame.width { - } else { + } else if immediatelyLayoutAccessoryPanelAndAnimateAppearance { transition.animatePositionAdditive(node: titleAccessoryPanelNode, offset: CGPoint(x: 0.0, y: -titleAccessoryPanelFrame.height)) + } else if previousFrame.minY != titleAccessoryPanelFrame.minY { + transition.animatePositionAdditive(node: titleAccessoryPanelNode, offset: CGPoint(x: 0.0, y: previousFrame.minY - titleAccessoryPanelFrame.minY)) } } @@ -2502,7 +2513,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition.updateFrame(node: dismissedTitleTopicsAccessoryPanelNode, frame: dismissedTopPanelFrame, completion: { [weak dismissedTitleTopicsAccessoryPanelNode] _ in dismissedTitleTopicsAccessoryPanelNode?.removeFromSupernode() }) - dismissedTitleTopicsAccessoryPanelNode.updateGlobalOffset(globalOffset: -dismissedTopPanelFrame.height, transition: ComponentTransition(transition)) + if self.leftPanel != nil { + dismissedTitleTopicsAccessoryPanelNode.updateGlobalOffset(globalOffset: -dismissedTopPanelFrame.height, transition: ComponentTransition(transition)) + } } if let dismissedTitleAccessoryPanelNode { @@ -2802,6 +2815,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { displayInlineSearch = true } } + if let channel = self.chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = self.chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.adminRights != nil { + if self.chatPresentationInterfaceState.search != nil { + displayInlineSearch = true + } + } if displayInlineSearch { let peerId = self.chatPresentationInterfaceState.chatLocation.peerId @@ -2827,6 +2845,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } else { mappedContents = .empty } + } else if let channel = self.chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = self.chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.adminRights != nil { + mappedContents = .monoforumChats(query: self.chatPresentationInterfaceState.search?.query ?? "") } else if case .peer(self.context.account.peerId) = self.chatPresentationInterfaceState.chatLocation { mappedContents = .tag(MemoryBuffer()) } else { @@ -2952,30 +2972,39 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { guard let self else { return } - guard let navigationController = self.controller?.navigationController as? NavigationController else { - return + + if let channel = self.chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum { + self.interfaceInteraction?.updateChatLocationThread(peer.id.toInt64()) + + self.controller?.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + return current.updatedSearch(nil) + }) + } else { + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: self.context, + chatLocation: .replyThread(ChatReplyThreadMessage( + peerId: self.context.account.peerId, + threadId: peer.id.toInt64(), + channelMessageId: nil, + isChannelPost: false, + isForumPost: false, + isMonoforumPost: false, + maxMessage: nil, + maxReadIncomingMessageId: nil, + maxReadOutgoingMessageId: nil, + unreadCount: 0, + initialFilledHoles: IndexSet(), + initialAnchor: .automatic, + isNotAvailable: false + )), + subject: nil, + keepStack: .always + )) } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( - navigationController: navigationController, - context: self.context, - chatLocation: .replyThread(ChatReplyThreadMessage( - peerId: self.context.account.peerId, - threadId: peer.id.toInt64(), - channelMessageId: nil, - isChannelPost: false, - isForumPost: false, - isMonoforumPost: false, - maxMessage: nil, - maxReadIncomingMessageId: nil, - maxReadOutgoingMessageId: nil, - unreadCount: 0, - initialFilledHoles: IndexSet(), - initialAnchor: .automatic, - isNotAvailable: false - )), - subject: nil, - keepStack: .always - )) }, loadTagMessages: { tag, index in let input: ChatHistoryLocationInput @@ -3048,6 +3077,89 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } return foundLocalPeers }, + getChats: { [weak self] query in + guard let self else { + return nil + } + guard let peerId = self.chatPresentationInterfaceState.chatLocation.peerId else { + return nil + } + + let viewKey: PostboxViewKey = .savedMessagesIndex(peerId: peerId) + let interfaceStateKey: PostboxViewKey = .chatInterfaceState(peerId: peerId) + + let accountPeerId = self.context.account.peerId + let threadListSignal: Signal = self.context.account.postbox.combinedView(keys: [viewKey, interfaceStateKey]) + |> map { views -> EngineChatList? in + guard let view = views.views[viewKey] as? MessageHistorySavedMessagesIndexView else { + preconditionFailure() + } + + var draft: EngineChatList.Draft? + if let interfaceStateView = views.views[interfaceStateKey] as? ChatInterfaceStateView { + if let embeddedState = interfaceStateView.value, let _ = embeddedState.overrideChatTimestamp { + if let opaqueState = _internal_decodeStoredChatInterfaceState(state: embeddedState) { + if let text = opaqueState.synchronizeableInputState?.text { + draft = EngineChatList.Draft(text: text, entities: opaqueState.synchronizeableInputState?.entities ?? []) + } + } + } + } + + var items: [EngineChatList.Item] = [] + for item in view.items { + guard let sourcePeer = item.peer else { + continue + } + + let sourceId = PeerId(item.id) + + var messages: [EngineMessage] = [] + if let topMessage = item.topMessage { + messages.append(EngineMessage(topMessage)) + } + + let mappedMessageIndex = MessageIndex(id: MessageId(peerId: sourceId, namespace: item.index.id.namespace, id: item.index.id.id), timestamp: item.index.timestamp) + + items.append(EngineChatList.Item( + id: .chatList(sourceId), + index: .chatList(ChatListIndex(pinningIndex: item.pinnedIndex.flatMap(UInt16.init), messageIndex: mappedMessageIndex)), + messages: messages, + readCounters: EnginePeerReadCounters( + incomingReadId: 0, outgoingReadId: 0, count: Int32(item.unreadCount), markedUnread: false), + isMuted: false, + draft: sourceId == accountPeerId ? draft : nil, + threadData: nil, + renderedPeer: EngineRenderedPeer(peer: EnginePeer(sourcePeer)), + presence: nil, + hasUnseenMentions: false, + hasUnseenReactions: false, + forumTopicData: nil, + topForumTopicItems: [], + hasFailed: false, + isContact: false, + autoremoveTimeout: nil, + storyStats: nil, + displayAsTopicList: false, + isPremiumRequiredToMessage: false, + mediaDraftContentType: nil + )) + } + + let list = EngineChatList( + items: items.reversed(), + groupItems: [], + additionalItems: [], + hasEarlier: false, + hasLater: false, + isLoading: view.isLoading + ) + + return list + } + + return threadListSignal + }, loadMoreSearchResults: { [weak self] in guard let self, let controller = self.controller else { return @@ -4052,7 +4164,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop, completion in self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop, completion: completion) - }, updateExtraNavigationBarBackgroundHeight: { _, _, _ in + }, updateExtraNavigationBarBackgroundHeight: { _, _, _, _ in }) } } diff --git a/submodules/TelegramUI/Sources/ChatControllerTitlePanelNodeContainer.swift b/submodules/TelegramUI/Sources/ChatControllerTitlePanelNodeContainer.swift index f6bf81862e..e73a848319 100644 --- a/submodules/TelegramUI/Sources/ChatControllerTitlePanelNodeContainer.swift +++ b/submodules/TelegramUI/Sources/ChatControllerTitlePanelNodeContainer.swift @@ -3,7 +3,13 @@ import UIKit import AsyncDisplayKit final class ChatControllerTitlePanelNodeContainer: ASDisplayNode { + var hitTestExcludeInsets = UIEdgeInsets() + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if point.x < self.hitTestExcludeInsets.left { + return nil + } + for subview in self.view.subviews { if let result = subview.hitTest(self.view.convert(point, to: subview), with: event) { return result diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index f4231397ae..a0dd97db61 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -232,13 +232,24 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat } func titleTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatTitleAccessoryPanelNode?, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, force: Bool) -> ChatTopicListTitleAccessoryPanelNode? { - if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.adminRights != nil { + if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.adminRights != nil, chatPresentationInterfaceState.search == nil { let topicListDisplayMode = chatPresentationInterfaceState.topicListDisplayMode ?? .top if case .top = topicListDisplayMode, let peerId = chatPresentationInterfaceState.chatLocation.peerId { if let currentPanel = currentPanel as? ChatTopicListTitleAccessoryPanelNode { return currentPanel } else { - let panel = ChatTopicListTitleAccessoryPanelNode(context: context, peerId: peerId) + let panel = ChatTopicListTitleAccessoryPanelNode(context: context, peerId: peerId, isMonoforum: true) + panel.interfaceInteraction = interfaceInteraction + return panel + } + } + } else if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForum, chatPresentationInterfaceState.search == nil { + let topicListDisplayMode = chatPresentationInterfaceState.topicListDisplayMode ?? .top + if case .top = topicListDisplayMode, let peerId = chatPresentationInterfaceState.chatLocation.peerId { + if let currentPanel = currentPanel as? ChatTopicListTitleAccessoryPanelNode { + return currentPanel + } else { + let panel = ChatTopicListTitleAccessoryPanelNode(context: context, peerId: peerId, isMonoforum: false) panel.interfaceInteraction = interfaceInteraction return panel } @@ -253,7 +264,7 @@ func sidePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState return nil } - if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.adminRights != nil { + if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.adminRights != nil, chatPresentationInterfaceState.search == nil { let topicListDisplayMode = chatPresentationInterfaceState.topicListDisplayMode ?? .top if case .side = topicListDisplayMode { return AnyComponentWithIdentity( @@ -263,6 +274,28 @@ func sidePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, peerId: peerId, + isMonoforum: true, + topicId: chatPresentationInterfaceState.chatLocation.threadId, + togglePanel: { [weak interfaceInteraction] in + interfaceInteraction?.toggleChatSidebarMode() + }, + updateTopicId: { [weak interfaceInteraction] topicId in + interfaceInteraction?.updateChatLocationThread(topicId) + } + )) + ) + } + } else if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForum, chatPresentationInterfaceState.search == nil { + let topicListDisplayMode = chatPresentationInterfaceState.topicListDisplayMode ?? .top + if case .side = topicListDisplayMode { + return AnyComponentWithIdentity( + id: "topics", + component: AnyComponent(ChatSideTopicsPanel( + context: context, + theme: chatPresentationInterfaceState.theme, + strings: chatPresentationInterfaceState.strings, + peerId: peerId, + isMonoforum: false, topicId: chatPresentationInterfaceState.chatLocation.threadId, togglePanel: { [weak interfaceInteraction] in interfaceInteraction?.toggleChatSidebarMode() diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 9262bd3f0d..35f4f9a047 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -517,12 +517,13 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { if self.currentLayout?.0 != width || self.currentLayout?.1 != leftInset || self.currentLayout?.2 != rightInset || messageUpdated || themeUpdated || currentTranslateToLanguageUpdated { self.currentLayout = (width, leftInset, rightInset) + let messageUpdated = self.currentMessage?.message.id != interfaceState.pinnedMessage?.message.id let previousMessageWasNil = self.currentMessage == nil self.currentMessage = interfaceState.pinnedMessage if let currentMessage = self.currentMessage, let currentLayout = self.currentLayout { self.dustNode?.update(revealed: false, animated: false) - self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread, translateToLanguage: translateToLanguage?.toLang) + self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: messageUpdated ? .immediate : transition, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread, translateToLanguage: translateToLanguage?.toLang) } } @@ -565,6 +566,8 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.textNode.textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -offset), to: CGPoint(), duration: 0.2, additive: true) self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } + } else { + animationTransition = transition } let makeTitleLayout = self.titleNode.asyncLayout() @@ -575,13 +578,6 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let previousMediaReference = self.previousMediaReference let context = self.context - let targetQueue: Queue - if firstTime { - targetQueue = Queue.mainQueue() - } else { - targetQueue = self.queue - } - let contentLeftInset: CGFloat = leftInset + 10.0 var textLineInset: CGFloat = 10.0 var rightInset: CGFloat = 14.0 + rightInset @@ -592,293 +588,288 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { rightInset += self.actionButton.bounds.width - 14.0 } - targetQueue.async { [weak self] in - var updatedMediaReference: AnyMediaReference? - var imageDimensions: CGSize? - - let giveaway = pinnedMessage.message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway - - var titleStrings: [AnimatedCountLabelNode.Segment] = [] - if let _ = giveaway { - titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedGiveaway) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) - } else { - if pinnedMessage.totalCount == 2 { - if pinnedMessage.index == 0 { - titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedPreviousMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) - } else { - titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) - } - } else if pinnedMessage.totalCount > 1 && pinnedMessage.index != pinnedMessage.totalCount - 1 { - titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage)", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) - titleStrings.append(.text(1, NSAttributedString(string: " #", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) - titleStrings.append(.number(pinnedMessage.index + 1, NSAttributedString(string: "\(pinnedMessage.index + 1)", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) + var updatedMediaReference: AnyMediaReference? + var imageDimensions: CGSize? + + let giveaway = pinnedMessage.message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway + + var titleStrings: [AnimatedCountLabelNode.Segment] = [] + if let _ = giveaway { + titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedGiveaway) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) + } else { + if pinnedMessage.totalCount == 2 { + if pinnedMessage.index == 0 { + titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedPreviousMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) } else { titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) } + } else if pinnedMessage.totalCount > 1 && pinnedMessage.index != pinnedMessage.totalCount - 1 { + titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage)", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) + titleStrings.append(.text(1, NSAttributedString(string: " #", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) + titleStrings.append(.number(pinnedMessage.index + 1, NSAttributedString(string: "\(pinnedMessage.index + 1)", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) + } else { + titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) } - - if !message.containsSecretMedia { - for media in message.media { - if let image = media as? TelegramMediaImage { - updatedMediaReference = .message(message: MessageReference(message), media: image) - if let representation = largestRepresentationForPhoto(image) { - imageDimensions = representation.dimensions.cgSize - } - break - } else if let file = media as? TelegramMediaFile { - updatedMediaReference = .message(message: MessageReference(message), media: file) - if !file.isInstantVideo && !file.isSticker, let representation = largestImageRepresentation(file.previewRepresentations) { - imageDimensions = representation.dimensions.cgSize - } else if file.isAnimated, let dimensions = file.dimensions { + } + + if !message.containsSecretMedia { + for media in message.media { + if let image = media as? TelegramMediaImage { + updatedMediaReference = .message(message: MessageReference(message), media: image) + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions.cgSize + } + break + } else if let file = media as? TelegramMediaFile { + updatedMediaReference = .message(message: MessageReference(message), media: file) + if !file.isInstantVideo && !file.isSticker, let representation = largestImageRepresentation(file.previewRepresentations) { + imageDimensions = representation.dimensions.cgSize + } else if file.isAnimated, let dimensions = file.dimensions { + imageDimensions = dimensions.cgSize + } + break + } else if let paidContent = media as? TelegramMediaPaidContent, let firstMedia = paidContent.extendedMedia.first { + switch firstMedia { + case let .preview(dimensions, immediateThumbnailData, _): + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + if let dimensions { imageDimensions = dimensions.cgSize } - break - } else if let paidContent = media as? TelegramMediaPaidContent, let firstMedia = paidContent.extendedMedia.first { - switch firstMedia { - case let .preview(dimensions, immediateThumbnailData, _): - let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - if let dimensions { + updatedMediaReference = .standalone(media: thumbnailMedia) + case let .full(fullMedia): + updatedMediaReference = .message(message: MessageReference(message), media: fullMedia) + if let image = fullMedia as? TelegramMediaImage { + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions.cgSize + } + break + } else if let file = fullMedia as? TelegramMediaFile { + if let dimensions = file.dimensions { imageDimensions = dimensions.cgSize } - updatedMediaReference = .standalone(media: thumbnailMedia) - case let .full(fullMedia): - updatedMediaReference = .message(message: MessageReference(message), media: fullMedia) - if let image = fullMedia as? TelegramMediaImage { - if let representation = largestRepresentationForPhoto(image) { - imageDimensions = representation.dimensions.cgSize - } - break - } else if let file = fullMedia as? TelegramMediaFile { - if let dimensions = file.dimensions { - imageDimensions = dimensions.cgSize - } - break - } - } - } - } - } - - if isReplyThread { - let titleString: String - if let author = message.effectiveAuthor { - titleString = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) - } else { - titleString = "" - } - titleStrings = [.text(0, NSAttributedString(string: titleString, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))] - } else { - for media in message.media { - if let media = media as? TelegramMediaInvoice { - titleStrings = [.text(0, NSAttributedString(string: media.title, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))] - break - } - } - } - - var applyImage: (() -> Void)? - if let imageDimensions = imageDimensions { - let boundingSize = CGSize(width: 35.0, height: 35.0) - applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) - - textLineInset += 9.0 + 35.0 - } - - var mediaUpdated = false - if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference { - mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media) - } else if (updatedMediaReference != nil) != (previousMediaReference != nil) { - mediaUpdated = true - } - - let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) - - var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - var updatedFetchMediaSignal: Signal? - if mediaUpdated { - if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { - if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { - if imageReference.media.representations.isEmpty { - updateImageSignal = chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, ignoreFullSize: true, synchronousLoad: true) - } else { - updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler) - } - } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { - if fileReference.media.isAnimatedSticker { - let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512) - updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))) - updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource)) - } else if fileReference.media.isVideo || fileReference.media.isAnimated { - updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: hasSpoiler) - } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { - updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation) - } - } - } else { - updateImageSignal = .single({ _ in return nil }) - } - } - let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .zero, titleStrings) - - let (textString, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) - - let messageText: NSAttributedString - let textFont = Font.regular(15.0) - if let giveaway { - let dateString = stringForDateWithoutYear(date: Date(timeIntervalSince1970: TimeInterval(giveaway.untilDate)), timeZone: .current, strings: strings) - let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - let isFinished = currentTime >= giveaway.untilDate - let text: String - if isFinished { - let winnersString = strings.Conversation_PinnedGiveaway_Finished_Winners(giveaway.quantity) - text = strings.Conversation_PinnedGiveaway_Finished(winnersString, dateString).string - } else { - let winnersString = strings.Conversation_PinnedGiveaway_Ongoing_Winners(giveaway.quantity) - text = strings.Conversation_PinnedGiveaway_Ongoing(winnersString, dateString).string - } - messageText = NSAttributedString(string: text, font: textFont, textColor: theme.chat.inputPanel.primaryTextColor) - } else if isText { - var text = message.text - var messageEntities = message.textEntitiesAttribute?.entities ?? [] - - if let translateToLanguage = translateToLanguage, !text.isEmpty { - for attribute in message.attributes { - if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { - text = attribute.text - messageEntities = attribute.entities break } } } - - let entities = messageEntities.filter { entity in - switch entity.type { - case .Spoiler, .CustomEmoji: - return true - default: - return false - } - } - let textColor = theme.chat.inputPanel.primaryTextColor - if entities.count > 0 { - messageText = stringWithAppliedEntities(trimToLineCount(text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) - } else { - messageText = NSAttributedString(string: foldLineBreaks(text), font: textFont, textColor: textColor) - } - } else { - messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor) } - - let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude) - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) - - let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? - if !textLayout.spoilers.isEmpty { - spoilerTextLayoutAndApply = makeSpoilerTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0), displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true)) + } + + if isReplyThread { + let titleString: String + if let author = message.effectiveAuthor { + titleString = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) } else { - spoilerTextLayoutAndApply = nil + titleString = "" } + titleStrings = [.text(0, NSAttributedString(string: titleString, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))] + } else { + for media in message.media { + if let media = media as? TelegramMediaInvoice { + titleStrings = [.text(0, NSAttributedString(string: media.title, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))] + break + } + } + } + + var applyImage: (() -> Void)? + if let imageDimensions = imageDimensions { + let boundingSize = CGSize(width: 35.0, height: 35.0) + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) - Queue.mainQueue().async { - if let strongSelf = self { - let _ = titleApply(animation != nil) - - var textArguments: TextNodeWithEntities.Arguments? - if let cache = strongSelf.animationCache, let renderer = strongSelf.animationRenderer { - textArguments = TextNodeWithEntities.Arguments( - context: strongSelf.context, - cache: cache, - renderer: renderer, - placeholderColor: theme.list.mediaPlaceholderColor, - attemptSynchronous: false - ) - } - let _ = textApply(textArguments) - - strongSelf.previousMediaReference = updatedMediaReference - - animationTransition.updateFrameAdditive(node: strongSelf.contentTextContainer, frame: CGRect(origin: CGPoint(x: contentLeftInset + textLineInset, y: 0.0), size: CGSize(width: width, height: panelHeight))) - - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 5.0), size: titleLayout.size) - - let textFrame = CGRect(origin: CGPoint(x: 0.0, y: 23.0), size: textLayout.size) - strongSelf.textNode.textNode.frame = textFrame - - if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { - let spoilerTextNode = spoilerTextApply(textArguments) - if strongSelf.spoilerTextNode == nil { - spoilerTextNode.textNode.alpha = 0.0 - spoilerTextNode.textNode.isUserInteractionEnabled = false - spoilerTextNode.textNode.contentMode = .topLeft - spoilerTextNode.textNode.contentsScale = UIScreenScale - spoilerTextNode.textNode.displaysAsynchronously = false - strongSelf.contentTextContainer.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textNode.textNode) - - strongSelf.spoilerTextNode = spoilerTextNode - } - - strongSelf.spoilerTextNode?.textNode.frame = textFrame - - let dustNode: InvisibleInkDustNode - if let current = strongSelf.dustNode { - dustNode = current - } else { - dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode, enableAnimations: strongSelf.context.sharedContext.energyUsageSettings.fullTranslucency) - strongSelf.dustNode = dustNode - strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode) - } - dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) - dustNode.update(size: dustNode.frame.size, color: theme.chat.inputPanel.secondaryTextColor, textColor: theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) - } else if let spoilerTextNode = strongSelf.spoilerTextNode { - strongSelf.spoilerTextNode = nil - spoilerTextNode.textNode.removeFromSupernode() - - if let dustNode = strongSelf.dustNode { - strongSelf.dustNode = nil - dustNode.removeFromSupernode() - } - } - - strongSelf.textNode.visibilityRect = CGRect.infinite - strongSelf.spoilerTextNode?.visibilityRect = CGRect.infinite - - let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight)) - animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame) - strongSelf.lineNode.update( - colors: AnimatedNavigationStripeNode.Colors( - foreground: theme.chat.inputPanel.panelControlAccentColor, - background: theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.5), - clearBackground: theme.chat.inputPanel.panelBackgroundColor - ), - configuration: AnimatedNavigationStripeNode.Configuration( - height: panelHeight, - index: pinnedMessage.index, - count: pinnedMessage.totalCount - ), - transition: animationTransition - ) - - strongSelf.imageNodeContainer.frame = CGRect(origin: CGPoint(x: contentLeftInset + 9.0, y: 7.0), size: CGSize(width: 35.0, height: 35.0)) - strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 35.0, height: 35.0)) - - if let applyImage = applyImage { - applyImage() - - animationTransition.updateSublayerTransformScale(node: strongSelf.imageNodeContainer, scale: 1.0) - animationTransition.updateAlpha(node: strongSelf.imageNodeContainer, alpha: 1.0, beginWithCurrentState: true) + textLineInset += 9.0 + 35.0 + } + + var mediaUpdated = false + if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference { + mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media) + } else if (updatedMediaReference != nil) != (previousMediaReference != nil) { + mediaUpdated = true + } + + let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) + + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var updatedFetchMediaSignal: Signal? + if mediaUpdated { + if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { + if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { + if imageReference.media.representations.isEmpty { + updateImageSignal = chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, ignoreFullSize: true, synchronousLoad: true) } else { - animationTransition.updateSublayerTransformScale(node: strongSelf.imageNodeContainer, scale: 0.1) - animationTransition.updateAlpha(node: strongSelf.imageNodeContainer, alpha: 0.0, beginWithCurrentState: true) + updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler) } - - if let updateImageSignal = updateImageSignal { - strongSelf.imageNode.setSignal(updateImageSignal) + } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { + if fileReference.media.isAnimatedSticker { + let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512) + updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))) + updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource)) + } else if fileReference.media.isVideo || fileReference.media.isAnimated { + updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: hasSpoiler) + } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation) } - if let updatedFetchMediaSignal = updatedFetchMediaSignal { - strongSelf.fetchDisposable.set(updatedFetchMediaSignal.startStrict()) + } + } else { + updateImageSignal = .single({ _ in return nil }) + } + } + let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .zero, titleStrings) + + let (textString, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) + + let messageText: NSAttributedString + let textFont = Font.regular(15.0) + if let giveaway { + let dateString = stringForDateWithoutYear(date: Date(timeIntervalSince1970: TimeInterval(giveaway.untilDate)), timeZone: .current, strings: strings) + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let isFinished = currentTime >= giveaway.untilDate + let text: String + if isFinished { + let winnersString = strings.Conversation_PinnedGiveaway_Finished_Winners(giveaway.quantity) + text = strings.Conversation_PinnedGiveaway_Finished(winnersString, dateString).string + } else { + let winnersString = strings.Conversation_PinnedGiveaway_Ongoing_Winners(giveaway.quantity) + text = strings.Conversation_PinnedGiveaway_Ongoing(winnersString, dateString).string + } + messageText = NSAttributedString(string: text, font: textFont, textColor: theme.chat.inputPanel.primaryTextColor) + } else if isText { + var text = message.text + var messageEntities = message.textEntitiesAttribute?.entities ?? [] + + if let translateToLanguage = translateToLanguage, !text.isEmpty { + for attribute in message.attributes { + if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { + text = attribute.text + messageEntities = attribute.entities + break } } } + + let entities = messageEntities.filter { entity in + switch entity.type { + case .Spoiler, .CustomEmoji: + return true + default: + return false + } + } + let textColor = theme.chat.inputPanel.primaryTextColor + if entities.count > 0 { + messageText = stringWithAppliedEntities(trimToLineCount(text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) + } else { + messageText = NSAttributedString(string: foldLineBreaks(text), font: textFont, textColor: textColor) + } + } else { + messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor) + } + + let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) + + let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? + if !textLayout.spoilers.isEmpty { + spoilerTextLayoutAndApply = makeSpoilerTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0), displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true)) + } else { + spoilerTextLayoutAndApply = nil + } + + let strongSelf = self + let _ = titleApply(animation != nil) + + var textArguments: TextNodeWithEntities.Arguments? + if let cache = strongSelf.animationCache, let renderer = strongSelf.animationRenderer { + textArguments = TextNodeWithEntities.Arguments( + context: strongSelf.context, + cache: cache, + renderer: renderer, + placeholderColor: theme.list.mediaPlaceholderColor, + attemptSynchronous: false + ) + } + let _ = textApply(textArguments) + + strongSelf.previousMediaReference = updatedMediaReference + + animationTransition.updateFrameAdditive(node: strongSelf.contentTextContainer, frame: CGRect(origin: CGPoint(x: contentLeftInset + textLineInset, y: 0.0), size: CGSize(width: width, height: panelHeight))) + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 5.0), size: titleLayout.size) + + let textFrame = CGRect(origin: CGPoint(x: 0.0, y: 23.0), size: textLayout.size) + strongSelf.textNode.textNode.frame = textFrame + + if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { + let spoilerTextNode = spoilerTextApply(textArguments) + if strongSelf.spoilerTextNode == nil { + spoilerTextNode.textNode.alpha = 0.0 + spoilerTextNode.textNode.isUserInteractionEnabled = false + spoilerTextNode.textNode.contentMode = .topLeft + spoilerTextNode.textNode.contentsScale = UIScreenScale + spoilerTextNode.textNode.displaysAsynchronously = false + strongSelf.contentTextContainer.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textNode.textNode) + + strongSelf.spoilerTextNode = spoilerTextNode + } + + strongSelf.spoilerTextNode?.textNode.frame = textFrame + + let dustNode: InvisibleInkDustNode + if let current = strongSelf.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode, enableAnimations: strongSelf.context.sharedContext.energyUsageSettings.fullTranslucency) + strongSelf.dustNode = dustNode + strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode) + } + dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + dustNode.update(size: dustNode.frame.size, color: theme.chat.inputPanel.secondaryTextColor, textColor: theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + } else if let spoilerTextNode = strongSelf.spoilerTextNode { + strongSelf.spoilerTextNode = nil + spoilerTextNode.textNode.removeFromSupernode() + + if let dustNode = strongSelf.dustNode { + strongSelf.dustNode = nil + dustNode.removeFromSupernode() + } + } + + strongSelf.textNode.visibilityRect = CGRect.infinite + strongSelf.spoilerTextNode?.visibilityRect = CGRect.infinite + + let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight)) + animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame) + strongSelf.lineNode.update( + colors: AnimatedNavigationStripeNode.Colors( + foreground: theme.chat.inputPanel.panelControlAccentColor, + background: theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.5), + clearBackground: theme.chat.inputPanel.panelBackgroundColor + ), + configuration: AnimatedNavigationStripeNode.Configuration( + height: panelHeight, + index: pinnedMessage.index, + count: pinnedMessage.totalCount + ), + transition: animationTransition + ) + + strongSelf.imageNodeContainer.frame = CGRect(origin: CGPoint(x: contentLeftInset + 9.0, y: 7.0), size: CGSize(width: 35.0, height: 35.0)) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 35.0, height: 35.0)) + + if let applyImage = applyImage { + applyImage() + + animationTransition.updateSublayerTransformScale(node: strongSelf.imageNodeContainer, scale: 1.0) + animationTransition.updateAlpha(node: strongSelf.imageNodeContainer, alpha: 1.0, beginWithCurrentState: true) + } else { + animationTransition.updateSublayerTransformScale(node: strongSelf.imageNodeContainer, scale: 0.1) + animationTransition.updateAlpha(node: strongSelf.imageNodeContainer, alpha: 0.0, beginWithCurrentState: true) + } + + if let updateImageSignal = updateImageSignal { + strongSelf.imageNode.setSignal(updateImageSignal) + } + if let updatedFetchMediaSignal = updatedFetchMediaSignal { + strongSelf.fetchDisposable.set(updatedFetchMediaSignal.startStrict()) } } diff --git a/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift index 9f529a7cd2..6a622d92c7 100644 --- a/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift @@ -246,7 +246,7 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C private let containerButton: HighlightTrackingButton - private let icon = ComponentView() + private var icon: ComponentView? private var avatarNode: AvatarNode? private let title = ComponentView() private var badge: ComponentView? @@ -319,33 +319,60 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C let spacing: CGFloat = 3.0 let badgeSpacing: CGFloat = 4.0 - let avatarIconContent: EmojiStatusComponent.Content - if case let .forum(topicId) = item.item.id, topicId != 1, let threadData = item.item.threadData { - if let fileId = threadData.info.icon, fileId != 0 { - avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(0)) + let iconSize = CGSize(width: 18.0, height: 18.0) + + var avatarIconContent: EmojiStatusComponent.Content? + if case let .forum(topicId) = item.item.id { + if topicId != 1, let threadData = item.item.threadData { + if let fileId = threadData.info.icon, fileId != 0 { + avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: iconSize, placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(0)) + } else { + avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: iconSize) + } } else { - avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: CGSize(width: 18.0, height: 18.0)) + avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicIcon(theme)) } - } else { - avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicIcon(theme)) } - let avatarIconComponent = EmojiStatusComponent( - context: context, - animationCache: context.animationCache, - animationRenderer: context.animationRenderer, - content: avatarIconContent, - isVisibleForAnimations: false, - action: nil - ) - let iconSize = self.icon.update( - transition: .immediate, - component: AnyComponent(avatarIconComponent), - environment: {}, - containerSize: CGSize(width: 18.0, height: 18.0) - ) + if let avatarIconContent { + let avatarIconComponent = EmojiStatusComponent( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + content: avatarIconContent, + isVisibleForAnimations: false, + action: nil + ) + let icon: ComponentView + if let current = self.icon { + icon = current + } else { + icon = ComponentView() + self.icon = icon + } + let _ = icon.update( + transition: .immediate, + component: AnyComponent(avatarIconComponent), + environment: {}, + containerSize: CGSize(width: 18.0, height: 18.0) + ) + } else if let icon = self.icon { + self.icon = nil + icon.view?.removeFromSuperview() + } - let titleText: String = item.item.renderedPeer.chatMainPeer?.compactDisplayTitle ?? " " + let titleText: String + if case let .forum(topicId) = item.item.id { + let _ = topicId + if let threadData = item.item.threadData { + titleText = threadData.info.title + } else { + //TODO:localize + titleText = "General" + } + } else { + titleText = item.item.renderedPeer.chatMainPeer?.compactDisplayTitle ?? " " + } let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -391,38 +418,37 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C let iconFrame = CGRect(origin: CGPoint(x: 0.0, y: 5.0 + floor((size.height - iconSize.height) * 0.5)), size: iconSize) let titleFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + spacing, y: 5.0 + floor((size.height - titleSize.height) * 0.5)), size: titleSize) - if let iconView = self.icon.view { - if iconView.superview == nil { - iconView.isUserInteractionEnabled = false - self.containerButton.addSubview(iconView) + if let icon = self.icon { + if let iconView = icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.containerButton.addSubview(iconView) + } + iconView.frame = iconFrame } - iconView.frame = iconFrame - if "".isEmpty { - iconView.isHidden = true - - let avatarNode: AvatarNode - if let current = self.avatarNode { - avatarNode = current - } else { - avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 7.0)) - self.avatarNode = avatarNode - self.containerButton.addSubview(avatarNode.view) - } - avatarNode.frame = iconFrame - avatarNode.updateSize(size: iconFrame.size) - - if let peer = item.item.renderedPeer.chatMainPeer { - if peer.smallProfileImage != nil { - avatarNode.setPeerV2(context: context, theme: theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) - } else { - avatarNode.setPeer(context: context, theme: theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) - } - } - } else if let avatarNode = self.avatarNode { + if let avatarNode = self.avatarNode { self.avatarNode = nil avatarNode.view.removeFromSuperview() - iconView.isHidden = false + } + } else { + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 7.0)) + self.avatarNode = avatarNode + self.containerButton.addSubview(avatarNode.view) + } + avatarNode.frame = iconFrame + avatarNode.updateSize(size: iconFrame.size) + + if let peer = item.item.renderedPeer.chatMainPeer { + if peer.smallProfileImage != nil { + avatarNode.setPeerV2(context: context, theme: theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) + } else { + avatarNode.setPeer(context: context, theme: theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) + } } } @@ -707,6 +733,7 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C } private let context: AccountContext + private let isMonoforum: Bool private let scrollView: ScrollView @@ -722,8 +749,9 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C private var appliedScrollToId: ScrollId? - init(context: AccountContext, peerId: EnginePeer.Id) { + init(context: AccountContext, peerId: EnginePeer.Id, isMonoforum: Bool) { self.context = context + self.isMonoforum = isMonoforum self.selectedLineView = UIImageView() @@ -751,78 +779,7 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C self.scrollView.disablesInteractiveTransitionGestureRecognizer = true - let viewKey: PostboxViewKey = .savedMessagesIndex(peerId: peerId) - let interfaceStateKey: PostboxViewKey = .chatInterfaceState(peerId: peerId) - - let accountPeerId = context.account.peerId - let threadListSignal: Signal = context.account.postbox.combinedView(keys: [viewKey, interfaceStateKey]) - |> map { views -> EngineChatList in - guard let view = views.views[viewKey] as? MessageHistorySavedMessagesIndexView else { - preconditionFailure() - } - - var draft: EngineChatList.Draft? - if let interfaceStateView = views.views[interfaceStateKey] as? ChatInterfaceStateView { - if let embeddedState = interfaceStateView.value, let _ = embeddedState.overrideChatTimestamp { - if let opaqueState = _internal_decodeStoredChatInterfaceState(state: embeddedState) { - if let text = opaqueState.synchronizeableInputState?.text { - draft = EngineChatList.Draft(text: text, entities: opaqueState.synchronizeableInputState?.entities ?? []) - } - } - } - } - - var items: [EngineChatList.Item] = [] - for item in view.items { - guard let sourcePeer = item.peer else { - continue - } - - let sourceId = PeerId(item.id) - - var messages: [EngineMessage] = [] - if let topMessage = item.topMessage { - messages.append(EngineMessage(topMessage)) - } - - let mappedMessageIndex = MessageIndex(id: MessageId(peerId: sourceId, namespace: item.index.id.namespace, id: item.index.id.id), timestamp: item.index.timestamp) - - items.append(EngineChatList.Item( - id: .chatList(sourceId), - index: .chatList(ChatListIndex(pinningIndex: item.pinnedIndex.flatMap(UInt16.init), messageIndex: mappedMessageIndex)), - messages: messages, - readCounters: EnginePeerReadCounters( - incomingReadId: 0, outgoingReadId: 0, count: Int32(item.unreadCount), markedUnread: false), - isMuted: false, - draft: sourceId == accountPeerId ? draft : nil, - threadData: nil, - renderedPeer: EngineRenderedPeer(peer: EnginePeer(sourcePeer)), - presence: nil, - hasUnseenMentions: false, - hasUnseenReactions: false, - forumTopicData: nil, - topForumTopicItems: [], - hasFailed: false, - isContact: false, - autoremoveTimeout: nil, - storyStats: nil, - displayAsTopicList: false, - isPremiumRequiredToMessage: false, - mediaDraftContentType: nil - )) - } - - let list = EngineChatList( - items: items.reversed(), - groupItems: [], - additionalItems: [], - hasEarlier: false, - hasLater: false, - isLoading: view.isLoading - ) - - return list - } + let threadListSignal: Signal = context.sharedContext.subscribeChatListData(context: context, location: isMonoforum ? .savedMessagesChats(peerId: peerId) : .forum(peerId: peerId)) self.itemsDisposable = (threadListSignal |> deliverOnMainQueue).startStrict(next: { [weak self] chatList in @@ -1001,8 +958,12 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C guard let self else { return } - let topicId = chatListItem.renderedPeer.peerId.toInt64() - self.interfaceInteraction?.updateChatLocationThread(topicId) + if case let .forum(topicId) = chatListItem.id { + self.interfaceInteraction?.updateChatLocationThread(topicId) + } else { + let topicId = chatListItem.renderedPeer.peerId.toInt64() + self.interfaceInteraction?.updateChatLocationThread(topicId) + } }, contextGesture: { gesture, sourceNode in }) self.itemViews[itemId] = itemView @@ -1010,8 +971,10 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C } var isSelected = false - if params.interfaceState.chatLocation.threadId == item.item.renderedPeer.peerId.toInt64() { - isSelected = true + if case let .forum(topicId) = item.item.id { + isSelected = params.interfaceState.chatLocation.threadId == topicId + } else { + isSelected = params.interfaceState.chatLocation.threadId == item.item.renderedPeer.peerId.toInt64() } let itemSize = itemView.update(context: self.context, item: item, isSelected: isSelected, theme: params.interfaceState.theme, height: panelHeight, transition: .immediate) let itemFrame = CGRect(origin: CGPoint(x: contentSize.width, y: -5.0), size: itemSize) @@ -1100,7 +1063,15 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C public func topicIndex(threadId: Int64?) -> Int? { if let threadId { - if let value = self.items.firstIndex(where: { $0.id == .chatList(PeerId(threadId)) }) { + if let value = self.items.firstIndex(where: { item in + if item.id == .chatList(PeerId(threadId)) { + return true + } else if item.id == .forum(threadId) { + return true + } else { + return false + } + }) { return value + 1 } else { return nil diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index b6d0f1c4d2..043b097745 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -805,7 +805,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection self.suspendedNavigationBarLayout = layout return } - self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, additionalCutout: nil, transition: transition) } override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -818,7 +818,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection self.suspendNavigationBarLayout = false if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout { self.suspendedNavigationBarLayout = suspendedNavigationBarLayout - self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, additionalCutout: nil, transition: transition) } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index e9c49ce330..1293225a72 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2238,6 +2238,13 @@ public final class SharedAccountContextImpl: SharedAccountContext { ) } + public func subscribeChatListData(context: AccountContext, location: ChatListControllerLocation) -> Signal { + return chatListViewForLocation(chatListLocation: location, location: .initial(count: 100, filter: nil), account: context.account, shouldLoadCanMessagePeer: false) + |> map { update -> EngineChatList in + return update.list + } + } + public func makePeerSharedMediaController(context: AccountContext, peerId: PeerId) -> ViewController? { return nil } diff --git a/submodules/TranslateUI/Sources/TranslateScreen.swift b/submodules/TranslateUI/Sources/TranslateScreen.swift index 776763d2b5..5ed5c5c428 100644 --- a/submodules/TranslateUI/Sources/TranslateScreen.swift +++ b/submodules/TranslateUI/Sources/TranslateScreen.swift @@ -1151,7 +1151,7 @@ public class TranslateScreen: ViewController { layout.statusBarHeight = nil - self.applyNavigationBarLayout(layout, navigationLayout: navigationLayout, additionalBackgroundHeight: 0.0, transition: transition) + self.applyNavigationBarLayout(layout, navigationLayout: navigationLayout, additionalBackgroundHeight: 0.0, additionalCutout: nil, transition: transition) } override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {