diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index e3479c2d1d..d063538c3c 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -471,6 +471,7 @@ public enum PeerInfoControllerMode { case nearbyPeer(distance: Int32) case group(PeerId) case reaction(MessageId) + case forumTopic(thread: ChatReplyThreadMessage) } public enum ContactListActionItemInlineIconPosition { @@ -749,7 +750,7 @@ public protocol SharedAccountContext: AnyObject { func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController - func makeChatQrCodeScreen(context: AccountContext, peer: Peer) -> ViewController + func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?) -> ViewController func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource) -> ViewController diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 458ac01c5a..54fe2bd0c3 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -81,6 +81,7 @@ swift_library( "//submodules/TelegramUI/Components/ForumCreateTopicScreen:ForumCreateTopicScreen", "//submodules/TelegramUI/Components/ChatTitleView", "//submodules/AnimationUI:AnimationUI", + "//submodules/PeerInfoUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index bdb64d1a0e..638eb80b59 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -39,6 +39,7 @@ import TelegramStringFormatting import ForumCreateTopicScreen import AnimationUI import ChatTitleView +import PeerInfoUI private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { if listNode.scroller.isDragging { @@ -342,6 +343,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private let selectAddMemberDisposable = MetaDisposable() private let addMemberDisposable = MetaDisposable() + private let joinForumDisposable = MetaDisposable() public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { if self.isNodeLoaded { @@ -499,6 +501,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController navigationController.replaceController(strongSelf, with: chatController, animated: true) } } + + if let channel = peerView.peers[peerView.peerId] as? TelegramChannel { + switch channel.participationStatus { + case .member: + strongSelf.setToolbar(nil, transition: .animated(duration: 0.4, curve: .spring)) + default: + //TODO:localize + strongSelf.setToolbar(Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: "Join", isEnabled: true)), transition: .animated(duration: 0.4, curve: .spring)) + } + } }) } } @@ -1158,6 +1170,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.activeDownloadsDisposable?.dispose() self.selectAddMemberDisposable.dispose() self.addMemberDisposable.dispose() + self.joinForumDisposable.dispose() } private func openStatusSetup(sourceView: UIView) { @@ -1311,6 +1324,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } strongSelf.deletePeerChat(peerId: peerId, joined: joined) } + self.chatListDisplayNode.containerNode.deletePeerThread = { [weak self] peerId, threadId in + guard let strongSelf = self else { + return + } + strongSelf.deletePeerThread(peerId: peerId, threadId: threadId) + } self.chatListDisplayNode.containerNode.peerSelected = { [weak self] peer, threadId, animated, activateInput, promoInfo in if let strongSelf = self { @@ -1540,8 +1559,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let context = strongSelf.context let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create) controller.navigationPresentation = .modal + controller.completion = { title, fileId in - let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconFileId: fileId) + let availableColors: [Int32] = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F] + + let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: availableColors.randomElement()!, iconFileId: fileId) |> deliverOnMainQueue).start(next: { topicId in let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, navigationController: navigationController, activateInput: .text).start() }) @@ -2536,8 +2558,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create) controller.navigationPresentation = .modal controller.completion = { title, fileId in - let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconFileId: fileId) - |> deliverOnMainQueue).start(next: { topicId in + let availableColors: [Int32] = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F] + + let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: availableColors.randomElement()!, iconFileId: fileId) + |> deliverOnMainQueue).start(next: { topicId in if let navigationController = (sourceController.navigationController as? NavigationController) { let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, navigationController: navigationController, activateInput: .text).start() } @@ -3150,22 +3174,74 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController ]) ]) self.present(actionSheet, in: .window(.root)) - } else if case .middle = action, !peerIds.isEmpty { - if case .chatList(.root) = self.location { - self.donePressed() - self.archiveChats(peerIds: Array(peerIds)) - } else { + } else if case .middle = action { + switch self.location { + case let .chatList(groupId): if !peerIds.isEmpty { - self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds.first!) - let _ = (self.context.engine.peers.updatePeersGroupIdInteractively(peerIds: Array(peerIds), groupId: .root) - |> deliverOnMainQueue).start(completed: { [weak self] in - guard let strongSelf = self else { + if groupId == .root { + self.donePressed() + self.archiveChats(peerIds: Array(peerIds)) + } else { + if !peerIds.isEmpty { + self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds.first!) + let _ = (self.context.engine.peers.updatePeersGroupIdInteractively(peerIds: Array(peerIds), groupId: .root) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil) + strongSelf.donePressed() + }) + } + } + } + case let .forum(peerId): + self.joinForumDisposable.set((self.context.peerChannelMemberCategoriesContextsManager.join(engine: context.engine, peerId: peerId, hash: nil) + |> afterDisposed { [weak self] in + Queue.mainQueue().async { + if let strongSelf = self { + let _ = strongSelf + /*strongSelf.activityIndicator.isHidden = true + strongSelf.activityIndicator.stopAnimating() + strongSelf.isJoining = false*/ + } + } + }).start(error: { [weak self] error in + guard let strongSelf = self else { + return + } + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let strongSelf = self, let peer = peer else { return } - strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil) - strongSelf.donePressed() + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + switch error { + case .inviteRequestSent: + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .inviteRequestSent(title: presentationData.strings.Group_RequestToJoinSent, text: presentationData.strings.Group_RequestToJoinSentDescriptionGroup ), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + return + case .tooMuchJoined: + (strongSelf.navigationController as? NavigationController)?.pushViewController(oldChannelsController(context: strongSelf.context, intent: .join, completed: { value in + if value { + self?.toolbarActionSelected(action: .middle) + } + })) + return + case .tooMuchUsers: + text = presentationData.strings.Conversation_UsersTooMuchError + case .generic: + if case let .channel(channel) = peer, case .broadcast = channel.info { + text = presentationData.strings.Channel_ErrorAccessDenied + } else { + text = presentationData.strings.Group_ErrorAccessDenied + } + } + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }) - } + })) } } } @@ -3368,7 +3444,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return false } if value == .commit { - let _ = strongSelf.context.engine.messages.clearHistoryInteractively(peerId: peerId, type: type).start(completed: { + let _ = strongSelf.context.engine.messages.clearHistoryInteractively(peerId: peerId, threadId: nil, type: type).start(completed: { guard let strongSelf = self else { return } @@ -3555,6 +3631,40 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) } + private func deletePeerThread(peerId: EnginePeer.Id, threadId: Int64) { + let actionSheet = ActionSheetController(presentationData: self.presentationData) + var items: [ActionSheetItem] = [] + + //TODO:localize + items.append(ActionSheetButtonItem(title: "Delete", color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + self?.commitDeletePeerThread(peerId: peerId, threadId: threadId) + })) + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + self.present(actionSheet, in: .window(.root)) + } + + private func commitDeletePeerThread(peerId: EnginePeer.Id, threadId: Int64) { + let _ = self.context.engine.peers.removeForumChannelThread(id: peerId, threadId: threadId).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.chatListDisplayNode.containerNode.updateState({ state in + var state = state + state.pendingRemovalPeerIds.remove(peerId) + return state + }) + strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil) + }) + } + public func maybeAskForPeerChatRemoval(peer: EngineRenderedPeer, joined: Bool = false, deleteGloballyIfPossible: Bool = false, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void) { guard let chatPeer = peer.peers[peer.peerId], let mainPeer = peer.chatMainPeer else { completion(false) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index f70f166ce4..17504f94b8 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -183,7 +183,7 @@ private final class ChatListShimmerNode: ASDisplayNode { let timestamp1: Int32 = 100000 let peers: [EnginePeer.Id: EnginePeer] = [:] let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in - }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in + }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() }, present: { _ in }) @@ -504,6 +504,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { previousItemNode.listNode.toggleArchivedFolderHiddenByDefault = nil previousItemNode.listNode.hidePsa = nil previousItemNode.listNode.deletePeerChat = nil + previousItemNode.listNode.deletePeerThread = nil previousItemNode.listNode.peerSelected = nil previousItemNode.listNode.groupSelected = nil previousItemNode.listNode.updatePeerGrouping = nil @@ -539,6 +540,9 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { itemNode.listNode.deletePeerChat = { [weak self] peerId, joined in self?.deletePeerChat?(peerId, joined) } + itemNode.listNode.deletePeerThread = { [weak self] peerId, threadId in + self?.deletePeerThread?(peerId, threadId) + } itemNode.listNode.peerSelected = { [weak self] peerId, threadId, animated, activateInput, promoInfo in self?.peerSelected?(peerId, threadId, animated, activateInput, promoInfo) } @@ -597,6 +601,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { var toggleArchivedFolderHiddenByDefault: (() -> Void)? var hidePsa: ((EnginePeer.Id) -> Void)? var deletePeerChat: ((EnginePeer.Id, Bool) -> Void)? + var deletePeerThread: ((EnginePeer.Id, Int64) -> Void)? var peerSelected: ((EnginePeer, Int64?, Bool, Bool, ChatListNodeEntryPromoInfo?) -> Void)? var groupSelected: ((EngineChatList.Group) -> Void)? var updatePeerGrouping: ((EnginePeer.Id, Bool) -> Void)? diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 35d5320074..b7f645a41c 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -1742,6 +1742,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in + }, deletePeerThread: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in }, toggleArchivedFolderHiddenByDefault: { @@ -2951,7 +2952,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { var peers: [EnginePeer.Id: EnginePeer] = [:] peers[peer1.id] = peer1 let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in - }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in + }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() }, present: { _ in }) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index e9220eeabe..925a64c66e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1956,7 +1956,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } let avatarIconContent: EmojiStatusComponent.Content - if let fileId = threadInfo.info.icon { + if let fileId = threadInfo.info.icon, fileId != 0 { avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: nil, loopMode: .forever) } else { avatarIconContent = .topic(title: String(threadInfo.info.title.prefix(1)), colorIndex: Int(clamping: abs(threadInfo.id)), size: CGSize(width: 32.0, height: 32.0)) @@ -2602,8 +2602,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + guard let item = self.item else { + return + } + var close = true - if let item = self.item, case let .chatList(index) = item.index { + if case let .chatList(index) = item.index { switch option.key { case RevealOptionKey.pin.rawValue: switch item.content { @@ -2680,6 +2684,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { default: break } + } else if case let .forum(_, threadId, _, _) = item.index, case let .forum(peerId) = item.chatListLocation { + switch option.key { + case RevealOptionKey.delete.rawValue: + item.interaction.deletePeerThread(peerId, threadId) + default: + break + } } if close { self.setRevealOptionsOpened(false, animated: true) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index e19761f9a1..9ab2211c87 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -68,6 +68,7 @@ public final class ChatListNodeInteraction { let setItemPinned: (EngineChatList.PinnedItem.Id, Bool) -> Void let setPeerMuted: (EnginePeer.Id, Bool) -> Void let deletePeer: (EnginePeer.Id, Bool) -> Void + let deletePeerThread: (EnginePeer.Id, Int64) -> Void let updatePeerGrouping: (EnginePeer.Id, Bool) -> Void let togglePeerMarkedUnread: (EnginePeer.Id, Bool) -> Void let toggleArchivedFolderHiddenByDefault: () -> Void @@ -98,6 +99,7 @@ public final class ChatListNodeInteraction { setItemPinned: @escaping (EngineChatList.PinnedItem.Id, Bool) -> Void, setPeerMuted: @escaping (EnginePeer.Id, Bool) -> Void, deletePeer: @escaping (EnginePeer.Id, Bool) -> Void, + deletePeerThread: @escaping (EnginePeer.Id, Int64) -> Void, updatePeerGrouping: @escaping (EnginePeer.Id, Bool) -> Void, togglePeerMarkedUnread: @escaping (EnginePeer.Id, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, @@ -118,6 +120,7 @@ public final class ChatListNodeInteraction { self.setItemPinned = setItemPinned self.setPeerMuted = setPeerMuted self.deletePeer = deletePeer + self.deletePeerThread = deletePeerThread self.updatePeerGrouping = updatePeerGrouping self.togglePeerMarkedUnread = togglePeerMarkedUnread self.toggleArchivedFolderHiddenByDefault = toggleArchivedFolderHiddenByDefault @@ -655,6 +658,7 @@ public final class ChatListNode: ListView { public var addContact: ((String) -> Void)? public var activateSearch: (() -> Void)? public var deletePeerChat: ((EnginePeer.Id, Bool) -> Void)? + public var deletePeerThread: ((EnginePeer.Id, Int64) -> Void)? public var updatePeerGrouping: ((EnginePeer.Id, Bool) -> Void)? public var presentAlert: ((String) -> Void)? public var present: ((ViewController) -> Void)? @@ -953,6 +957,8 @@ public final class ChatListNode: ListView { }) }, deletePeer: { [weak self] peerId, joined in self?.deletePeerChat?(peerId, joined) + }, deletePeerThread: { [weak self] peerId, threadId in + self?.deletePeerThread?(peerId, threadId) }, updatePeerGrouping: { [weak self] peerId, group in self?.updatePeerGrouping?(peerId, group) }, togglePeerMarkedUnread: { [weak self, weak context] peerId, animated in diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 2e1f0c103a..e3cbe7d661 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -487,7 +487,7 @@ public class ContactsController: ViewController { let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId) |> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in if let strongSelf = self, let controller = controller { - controller.present(strongSelf.context.sharedContext.makeChatQrCodeScreen(context: strongSelf.context, peer: peer), in: .window(.root)) + controller.present(strongSelf.context.sharedContext.makeChatQrCodeScreen(context: strongSelf.context, peer: peer, threadId: nil), in: .window(.root)) } }) } diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 5d51a5cbd6..cc809e48d7 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -77,6 +77,7 @@ public final class HashtagSearchController: TelegramBaseController { }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in + }, deletePeerThread: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in }, toggleArchivedFolderHiddenByDefault: { diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index 57689a4d46..779f117e26 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -470,16 +470,42 @@ final class MessageHistoryTable: Table { self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, updatedMedia: &updatedMedia, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations, globalTagsOperations: &globalTagsOperations, pendingActionsOperations: &pendingActionsOperations, updatedMessageActionsSummaries: &updatedMessageActionsSummaries, updatedMessageTagSummaries: &updatedMessageTagSummaries, invalidateMessageTagSummaries: &invalidateMessageTagSummaries, localTagsOperations: &localTagsOperations, timestampBasedMessageAttributesOperations: ×tampBasedMessageAttributesOperations) } - func clearHistoryInRange(peerId: PeerId, minTimestamp: Int32, maxTimestamp: Int32, namespaces: MessageIdNamespaces, operationsByPeerId: inout [PeerId: [MessageHistoryOperation]], updatedMedia: inout [MediaId: Media?], unsentMessageOperations: inout [IntermediateMessageHistoryUnsentOperation], updatedPeerReadStateOperations: inout [PeerId: PeerReadStateSynchronizationOperation?], globalTagsOperations: inout [GlobalMessageHistoryTagsOperation], pendingActionsOperations: inout [PendingMessageActionsOperation], updatedMessageActionsSummaries: inout [PendingMessageActionsSummaryKey: Int32], updatedMessageTagSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateMessageTagSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation], localTagsOperations: inout [IntermediateMessageHistoryLocalTagsOperation], timestampBasedMessageAttributesOperations: inout [TimestampBasedMessageAttributesOperation], forEachMedia: (Media) -> Void) { + func clearHistoryInRange(peerId: PeerId, threadId: Int64?, minTimestamp: Int32, maxTimestamp: Int32, namespaces: MessageIdNamespaces, operationsByPeerId: inout [PeerId: [MessageHistoryOperation]], updatedMedia: inout [MediaId: Media?], unsentMessageOperations: inout [IntermediateMessageHistoryUnsentOperation], updatedPeerReadStateOperations: inout [PeerId: PeerReadStateSynchronizationOperation?], globalTagsOperations: inout [GlobalMessageHistoryTagsOperation], pendingActionsOperations: inout [PendingMessageActionsOperation], updatedMessageActionsSummaries: inout [PendingMessageActionsSummaryKey: Int32], updatedMessageTagSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateMessageTagSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation], localTagsOperations: inout [IntermediateMessageHistoryLocalTagsOperation], timestampBasedMessageAttributesOperations: inout [TimestampBasedMessageAttributesOperation], forEachMedia: (Media) -> Void) { var indices = self.allMessageIndices(peerId: peerId).filter { namespaces.contains($0.id.namespace) } + if let threadId = threadId { + indices = indices.filter { index in + if let message = self.getMessage(index) { + if message.threadId == threadId { + return true + } else { + return false + } + } else { + return false + } + } + } indices = indices.filter { index in return index.timestamp >= minTimestamp && index.timestamp <= maxTimestamp } self.removeMessages(indices.map { $0.id }, operationsByPeerId: &operationsByPeerId, updatedMedia: &updatedMedia, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations, globalTagsOperations: &globalTagsOperations, pendingActionsOperations: &pendingActionsOperations, updatedMessageActionsSummaries: &updatedMessageActionsSummaries, updatedMessageTagSummaries: &updatedMessageTagSummaries, invalidateMessageTagSummaries: &invalidateMessageTagSummaries, localTagsOperations: &localTagsOperations, timestampBasedMessageAttributesOperations: ×tampBasedMessageAttributesOperations, forEachMedia: forEachMedia) } - func clearHistory(peerId: PeerId, namespaces: MessageIdNamespaces, operationsByPeerId: inout [PeerId: [MessageHistoryOperation]], updatedMedia: inout [MediaId: Media?], unsentMessageOperations: inout [IntermediateMessageHistoryUnsentOperation], updatedPeerReadStateOperations: inout [PeerId: PeerReadStateSynchronizationOperation?], globalTagsOperations: inout [GlobalMessageHistoryTagsOperation], pendingActionsOperations: inout [PendingMessageActionsOperation], updatedMessageActionsSummaries: inout [PendingMessageActionsSummaryKey: Int32], updatedMessageTagSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateMessageTagSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation], localTagsOperations: inout [IntermediateMessageHistoryLocalTagsOperation], timestampBasedMessageAttributesOperations: inout [TimestampBasedMessageAttributesOperation], forEachMedia: (Media) -> Void) { - let indices = self.allMessageIndices(peerId: peerId).filter { namespaces.contains($0.id.namespace) } + func clearHistory(peerId: PeerId, threadId: Int64?, namespaces: MessageIdNamespaces, operationsByPeerId: inout [PeerId: [MessageHistoryOperation]], updatedMedia: inout [MediaId: Media?], unsentMessageOperations: inout [IntermediateMessageHistoryUnsentOperation], updatedPeerReadStateOperations: inout [PeerId: PeerReadStateSynchronizationOperation?], globalTagsOperations: inout [GlobalMessageHistoryTagsOperation], pendingActionsOperations: inout [PendingMessageActionsOperation], updatedMessageActionsSummaries: inout [PendingMessageActionsSummaryKey: Int32], updatedMessageTagSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateMessageTagSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation], localTagsOperations: inout [IntermediateMessageHistoryLocalTagsOperation], timestampBasedMessageAttributesOperations: inout [TimestampBasedMessageAttributesOperation], forEachMedia: (Media) -> Void) { + var indices = self.allMessageIndices(peerId: peerId).filter { namespaces.contains($0.id.namespace) } + if let threadId = threadId { + indices = indices.filter { index in + if let message = self.getMessage(index) { + if message.threadId == threadId { + return true + } else { + return false + } + } else { + return false + } + } + } self.removeMessages(indices.map { $0.id }, operationsByPeerId: &operationsByPeerId, updatedMedia: &updatedMedia, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations, globalTagsOperations: &globalTagsOperations, pendingActionsOperations: &pendingActionsOperations, updatedMessageActionsSummaries: &updatedMessageActionsSummaries, updatedMessageTagSummaries: &updatedMessageTagSummaries, invalidateMessageTagSummaries: &invalidateMessageTagSummaries, localTagsOperations: &localTagsOperations, timestampBasedMessageAttributesOperations: ×tampBasedMessageAttributesOperations, forEachMedia: forEachMedia) } diff --git a/submodules/Postbox/Sources/MessageThreadIndexTable.swift b/submodules/Postbox/Sources/MessageThreadIndexTable.swift index dd2c7ce086..bbc9f159d3 100644 --- a/submodules/Postbox/Sources/MessageThreadIndexTable.swift +++ b/submodules/Postbox/Sources/MessageThreadIndexTable.swift @@ -64,11 +64,15 @@ class MessageHistoryThreadIndexTable: Table { return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true) } + private struct UpdatedEntry { + var value: CodableEntry? + } + private let reverseIndexTable: MessageHistoryThreadReverseIndexTable private let sharedKey = ValueBoxKey(length: 8 + 4 + 8 + 4 + 4) - private var updatedInfoItems: [MessageHistoryThreadsTable.ItemId: CodableEntry] = [:] + private var updatedInfoItems: [MessageHistoryThreadsTable.ItemId: UpdatedEntry] = [:] init(valueBox: ValueBox, table: ValueBoxTable, reverseIndexTable: MessageHistoryThreadReverseIndexTable, useCaches: Bool) { self.reverseIndexTable = reverseIndexTable @@ -112,7 +116,7 @@ class MessageHistoryThreadIndexTable: Table { func get(peerId: PeerId, threadId: Int64) -> CodableEntry? { if let updated = self.updatedInfoItems[MessageHistoryThreadsTable.ItemId(peerId: peerId, threadId: threadId)] { - return updated + return updated.value } else { if let itemIndex = self.reverseIndexTable.get(peerId: peerId, threadId: threadId) { if let value = self.valueBox.get(self.table, key: self.key(peerId: itemIndex.id.peerId, timestamp: itemIndex.timestamp, threadId: threadId, namespace: itemIndex.id.namespace, id: itemIndex.id.id, key: self.sharedKey)) { @@ -130,8 +134,8 @@ class MessageHistoryThreadIndexTable: Table { } } - func set(peerId: PeerId, threadId: Int64, info: CodableEntry) { - self.updatedInfoItems[MessageHistoryThreadsTable.ItemId(peerId: peerId, threadId: threadId)] = info + func set(peerId: PeerId, threadId: Int64, info: CodableEntry?) { + self.updatedInfoItems[MessageHistoryThreadsTable.ItemId(peerId: peerId, threadId: threadId)] = UpdatedEntry(value: info) } func replay(threadsTable: MessageHistoryThreadsTable, namespaces: Set, updatedIds: Set) -> Set { @@ -155,7 +159,11 @@ class MessageHistoryThreadIndexTable: Table { self.valueBox.remove(self.table, key: previousKey, secure: true) } if let updatedInfo = self.updatedInfoItems[itemId] { - info = ReadBuffer(data: updatedInfo.data) + if let value = updatedInfo.value { + info = ReadBuffer(data: value.data) + } else { + info = nil + } } if let topIndex = topIndex, let info = info { diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 922ac575eb..207bc12cbc 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -151,9 +151,9 @@ public final class Transaction { self.postbox?.withAllMessages(peerId: peerId, namespace: namespace, f) } - public func clearHistory(_ peerId: PeerId, minTimestamp: Int32?, maxTimestamp: Int32?, namespaces: MessageIdNamespaces, forEachMedia: (Media) -> Void) { + public func clearHistory(_ peerId: PeerId, threadId: Int64?, minTimestamp: Int32?, maxTimestamp: Int32?, namespaces: MessageIdNamespaces, forEachMedia: (Media) -> Void) { assert(!self.disposed) - self.postbox?.clearHistory(peerId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, forEachMedia: forEachMedia) + self.postbox?.clearHistory(peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, forEachMedia: forEachMedia) } public func removeAllMessagesWithAuthor(_ peerId: PeerId, authorId: PeerId, namespace: MessageId.Namespace, forEachMedia: (Media) -> Void) { @@ -1169,7 +1169,7 @@ public final class Transaction { return self.postbox!.messageHistoryThreadIndexTable.get(peerId: peerId, threadId: threadId) } - public func setMessageHistoryThreadInfo(peerId: PeerId, threadId: Int64, info: CodableEntry) { + public func setMessageHistoryThreadInfo(peerId: PeerId, threadId: Int64, info: CodableEntry?) { assert(!self.disposed) self.postbox!.messageHistoryThreadIndexTable.set(peerId: peerId, threadId: threadId, info: info) } @@ -1864,11 +1864,11 @@ final class PostboxImpl { } } - fileprivate func clearHistory(_ peerId: PeerId, minTimestamp: Int32?, maxTimestamp: Int32?, namespaces: MessageIdNamespaces, forEachMedia: (Media) -> Void) { + fileprivate func clearHistory(_ peerId: PeerId, threadId: Int64?, minTimestamp: Int32?, maxTimestamp: Int32?, namespaces: MessageIdNamespaces, forEachMedia: (Media) -> Void) { if let minTimestamp = minTimestamp, let maxTimestamp = maxTimestamp { - self.messageHistoryTable.clearHistoryInRange(peerId: peerId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, operationsByPeerId: &self.currentOperationsByPeerId, updatedMedia: &self.currentUpdatedMedia, unsentMessageOperations: ¤tUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations, globalTagsOperations: &self.currentGlobalTagsOperations, pendingActionsOperations: &self.currentPendingMessageActionsOperations, updatedMessageActionsSummaries: &self.currentUpdatedMessageActionsSummaries, updatedMessageTagSummaries: &self.currentUpdatedMessageTagSummaries, invalidateMessageTagSummaries: &self.currentInvalidateMessageTagSummaries, localTagsOperations: &self.currentLocalTagsOperations, timestampBasedMessageAttributesOperations: &self.currentTimestampBasedMessageAttributesOperations, forEachMedia: forEachMedia) + self.messageHistoryTable.clearHistoryInRange(peerId: peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, operationsByPeerId: &self.currentOperationsByPeerId, updatedMedia: &self.currentUpdatedMedia, unsentMessageOperations: ¤tUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations, globalTagsOperations: &self.currentGlobalTagsOperations, pendingActionsOperations: &self.currentPendingMessageActionsOperations, updatedMessageActionsSummaries: &self.currentUpdatedMessageActionsSummaries, updatedMessageTagSummaries: &self.currentUpdatedMessageTagSummaries, invalidateMessageTagSummaries: &self.currentInvalidateMessageTagSummaries, localTagsOperations: &self.currentLocalTagsOperations, timestampBasedMessageAttributesOperations: &self.currentTimestampBasedMessageAttributesOperations, forEachMedia: forEachMedia) } else { - self.messageHistoryTable.clearHistory(peerId: peerId, namespaces: namespaces, operationsByPeerId: &self.currentOperationsByPeerId, updatedMedia: &self.currentUpdatedMedia, unsentMessageOperations: ¤tUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations, globalTagsOperations: &self.currentGlobalTagsOperations, pendingActionsOperations: &self.currentPendingMessageActionsOperations, updatedMessageActionsSummaries: &self.currentUpdatedMessageActionsSummaries, updatedMessageTagSummaries: &self.currentUpdatedMessageTagSummaries, invalidateMessageTagSummaries: &self.currentInvalidateMessageTagSummaries, localTagsOperations: &self.currentLocalTagsOperations, timestampBasedMessageAttributesOperations: &self.currentTimestampBasedMessageAttributesOperations, forEachMedia: forEachMedia) + self.messageHistoryTable.clearHistory(peerId: peerId, threadId: threadId, namespaces: namespaces, operationsByPeerId: &self.currentOperationsByPeerId, updatedMedia: &self.currentUpdatedMedia, unsentMessageOperations: ¤tUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations, globalTagsOperations: &self.currentGlobalTagsOperations, pendingActionsOperations: &self.currentPendingMessageActionsOperations, updatedMessageActionsSummaries: &self.currentUpdatedMessageActionsSummaries, updatedMessageTagSummaries: &self.currentUpdatedMessageTagSummaries, invalidateMessageTagSummaries: &self.currentInvalidateMessageTagSummaries, localTagsOperations: &self.currentLocalTagsOperations, timestampBasedMessageAttributesOperations: &self.currentTimestampBasedMessageAttributesOperations, forEachMedia: forEachMedia) for namespace in self.messageHistoryHoleIndexTable.existingNamespaces(peerId: peerId, holeSpace: .everywhere) where namespaces.contains(namespace) { self.messageHistoryHoleIndexTable.remove(peerId: peerId, namespace: namespace, space: .everywhere, range: 1 ... Int32.max - 1, operations: &self.currentPeerHoleOperations) } diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index b2836c5c3a..c14e513cbe 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -219,7 +219,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView var items: [ChatListItem] = [] let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in - }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in + }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() }, present: { _ in }) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index 833a41e9f2..585c12bf24 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -839,7 +839,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate var items: [ChatListItem] = [] let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in - }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in + }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() }, present: { _ in diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index f28f26218a..11f768e950 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -363,7 +363,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { var items: [ChatListItem] = [] let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in - }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in + }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() }, present: { _ in diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 8a3f3f04d6..c9ece079d2 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -222,7 +222,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-207944868] = { return Api.FileHash.parse_fileHash($0) } dict[-11252123] = { return Api.Folder.parse_folder($0) } dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($0) } - dict[792599046] = { return Api.ForumTopic.parse_forumTopic($0) } + dict[413771876] = { return Api.ForumTopic.parse_forumTopic($0) } dict[-1107729093] = { return Api.Game.parse_game($0) } dict[-1297942941] = { return Api.GeoPoint.parse_geoPoint($0) } dict[286776671] = { return Api.GeoPoint.parse_geoPointEmpty($0) } @@ -446,9 +446,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[455635795] = { return Api.MessageAction.parse_messageActionSecureValuesSentMe($0) } dict[-1434950843] = { return Api.MessageAction.parse_messageActionSetChatTheme($0) } dict[-1441072131] = { return Api.MessageAction.parse_messageActionSetMessagesTTL($0) } - dict[1873254060] = { return Api.MessageAction.parse_messageActionTopicCreate($0) } - dict[-2113245653] = { return Api.MessageAction.parse_messageActionTopicEditIcon($0) } - dict[-838130739] = { return Api.MessageAction.parse_messageActionTopicEditTitle($0) } + dict[228168278] = { return Api.MessageAction.parse_messageActionTopicCreate($0) } + dict[-1323849227] = { return Api.MessageAction.parse_messageActionTopicEdit($0) } dict[-1262252875] = { return Api.MessageAction.parse_messageActionWebViewDataSent($0) } dict[1205698681] = { return Api.MessageAction.parse_messageActionWebViewDataSentMe($0) } dict[546203849] = { return Api.MessageEntity.parse_inputMessageEntityMentionName($0) } diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index b6366ce19f..cc64b39a7b 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -1019,9 +1019,8 @@ public extension Api { case messageActionSecureValuesSentMe(values: [Api.SecureValue], credentials: Api.SecureCredentialsEncrypted) case messageActionSetChatTheme(emoticon: String) case messageActionSetMessagesTTL(period: Int32) - case messageActionTopicCreate(flags: Int32, title: String, iconEmojiId: Int64?) - case messageActionTopicEditIcon(emojiDocumentId: Int64) - case messageActionTopicEditTitle(title: String) + case messageActionTopicCreate(flags: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?) + case messageActionTopicEdit(flags: Int32, title: String?, iconEmojiId: Int64?) case messageActionWebViewDataSent(text: String) case messageActionWebViewDataSentMe(text: String, data: String) @@ -1257,25 +1256,22 @@ public extension Api { } serializeInt32(period, buffer: buffer, boxed: false) break - case .messageActionTopicCreate(let flags, let title, let iconEmojiId): + case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): if boxed { - buffer.appendInt32(1873254060) + buffer.appendInt32(228168278) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) + serializeInt32(iconColor, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt64(iconEmojiId!, buffer: buffer, boxed: false)} break - case .messageActionTopicEditIcon(let emojiDocumentId): + case .messageActionTopicEdit(let flags, let title, let iconEmojiId): if boxed { - buffer.appendInt32(-2113245653) + buffer.appendInt32(-1323849227) } - serializeInt64(emojiDocumentId, buffer: buffer, boxed: false) - break - case .messageActionTopicEditTitle(let title): - if boxed { - buffer.appendInt32(-838130739) - } - serializeString(title, buffer: buffer, boxed: false) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeInt64(iconEmojiId!, buffer: buffer, boxed: false)} break case .messageActionWebViewDataSent(let text): if boxed { @@ -1357,12 +1353,10 @@ public extension Api { return ("messageActionSetChatTheme", [("emoticon", String(describing: emoticon))]) case .messageActionSetMessagesTTL(let period): return ("messageActionSetMessagesTTL", [("period", String(describing: period))]) - case .messageActionTopicCreate(let flags, let title, let iconEmojiId): - return ("messageActionTopicCreate", [("flags", String(describing: flags)), ("title", String(describing: title)), ("iconEmojiId", String(describing: iconEmojiId))]) - case .messageActionTopicEditIcon(let emojiDocumentId): - return ("messageActionTopicEditIcon", [("emojiDocumentId", String(describing: emojiDocumentId))]) - case .messageActionTopicEditTitle(let title): - return ("messageActionTopicEditTitle", [("title", String(describing: title))]) + case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): + return ("messageActionTopicCreate", [("flags", String(describing: flags)), ("title", String(describing: title)), ("iconColor", String(describing: iconColor)), ("iconEmojiId", String(describing: iconEmojiId))]) + case .messageActionTopicEdit(let flags, let title, let iconEmojiId): + return ("messageActionTopicEdit", [("flags", String(describing: flags)), ("title", String(describing: title)), ("iconEmojiId", String(describing: iconEmojiId))]) case .messageActionWebViewDataSent(let text): return ("messageActionWebViewDataSent", [("text", String(describing: text))]) case .messageActionWebViewDataSentMe(let text, let data): @@ -1762,35 +1756,33 @@ public extension Api { _1 = reader.readInt32() var _2: String? _2 = parseString(reader) - var _3: Int64? - if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt64() } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int64? + if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.MessageAction.messageActionTopicCreate(flags: _1!, title: _2!, iconColor: _3!, iconEmojiId: _4) + } + else { + return nil + } + } + public static func parse_messageActionTopicEdit(_ reader: BufferReader) -> MessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } + var _3: Int64? + if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt64() } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil if _c1 && _c2 && _c3 { - return Api.MessageAction.messageActionTopicCreate(flags: _1!, title: _2!, iconEmojiId: _3) - } - else { - return nil - } - } - public static func parse_messageActionTopicEditIcon(_ reader: BufferReader) -> MessageAction? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.MessageAction.messageActionTopicEditIcon(emojiDocumentId: _1!) - } - else { - return nil - } - } - public static func parse_messageActionTopicEditTitle(_ reader: BufferReader) -> MessageAction? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.MessageAction.messageActionTopicEditTitle(title: _1!) + return Api.MessageAction.messageActionTopicEdit(flags: _1!, title: _2, iconEmojiId: _3) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index d3ad69554b..2232c9c612 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -1854,16 +1854,17 @@ public extension Api.functions.channels { } } public extension Api.functions.channels { - static func createForumTopic(flags: Int32, channel: Api.InputChannel, title: String, iconEmojiId: Int64?, randomId: Int64, sendAs: Api.InputPeer?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func createForumTopic(flags: Int32, channel: Api.InputChannel, title: String, iconColor: Int32?, iconEmojiId: Int64?, randomId: Int64, sendAs: Api.InputPeer?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1623145417) + buffer.appendInt32(-200539612) serializeInt32(flags, buffer: buffer, boxed: false) channel.serialize(buffer, true) serializeString(title, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(iconColor!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {serializeInt64(iconEmojiId!, buffer: buffer, boxed: false)} serializeInt64(randomId, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {sendAs!.serialize(buffer, true)} - return (FunctionDescription(name: "channels.createForumTopic", parameters: [("flags", String(describing: flags)), ("channel", String(describing: channel)), ("title", String(describing: title)), ("iconEmojiId", String(describing: iconEmojiId)), ("randomId", String(describing: randomId)), ("sendAs", String(describing: sendAs))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "channels.createForumTopic", parameters: [("flags", String(describing: flags)), ("channel", String(describing: channel)), ("title", String(describing: title)), ("iconColor", String(describing: iconColor)), ("iconEmojiId", String(describing: iconEmojiId)), ("randomId", String(describing: randomId)), ("sendAs", String(describing: sendAs))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -1941,6 +1942,22 @@ public extension Api.functions.channels { }) } } +public extension Api.functions.channels { + static func deleteTopicHistory(channel: Api.InputChannel, topMsgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(876830509) + channel.serialize(buffer, true) + serializeInt32(topMsgId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "channels.deleteTopicHistory", parameters: [("channel", String(describing: channel)), ("topMsgId", String(describing: topMsgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.AffectedHistory? in + let reader = BufferReader(buffer) + var result: Api.messages.AffectedHistory? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.AffectedHistory + } + return result + }) + } +} public extension Api.functions.channels { static func editAdmin(channel: Api.InputChannel, userId: Api.InputUser, adminRights: Api.ChatAdminRights, rank: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -1994,30 +2011,15 @@ public extension Api.functions.channels { } } public extension Api.functions.channels { - static func editForumIcon(channel: Api.InputChannel, topicId: Int32, emojiDocumentId: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func editForumTopic(flags: Int32, channel: Api.InputChannel, topicId: Int32, title: String?, iconEmojiId: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1439605469) + buffer.appendInt32(-1971370421) + serializeInt32(flags, buffer: buffer, boxed: false) channel.serialize(buffer, true) serializeInt32(topicId, buffer: buffer, boxed: false) - serializeInt64(emojiDocumentId, buffer: buffer, boxed: false) - return (FunctionDescription(name: "channels.editForumIcon", parameters: [("channel", String(describing: channel)), ("topicId", String(describing: topicId)), ("emojiDocumentId", String(describing: emojiDocumentId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in - let reader = BufferReader(buffer) - var result: Api.Updates? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Updates - } - return result - }) - } -} -public extension Api.functions.channels { - static func editForumTitle(channel: Api.InputChannel, topicId: Int32, title: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-2004764239) - channel.serialize(buffer, true) - serializeInt32(topicId, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - return (FunctionDescription(name: "channels.editForumTitle", parameters: [("channel", String(describing: channel)), ("topicId", String(describing: topicId)), ("title", String(describing: title))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 0) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeInt64(iconEmojiId!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "channels.editForumTopic", parameters: [("flags", String(describing: flags)), ("channel", String(describing: channel)), ("topicId", String(describing: topicId)), ("title", String(describing: title)), ("iconEmojiId", String(describing: iconEmojiId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index ab3f199352..f88470e925 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -1094,18 +1094,19 @@ public extension Api { } public extension Api { enum ForumTopic: TypeConstructorDescription { - case forumTopic(flags: Int32, id: Int32, date: Int32, title: String, iconEmojiId: Int64?, topMessage: Int32, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, unreadMentionsCount: Int32, unreadReactionsCount: Int32) + case forumTopic(flags: Int32, id: Int32, date: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?, topMessage: Int32, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, unreadMentionsCount: Int32, unreadReactionsCount: Int32, fromId: Api.Peer) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .forumTopic(let flags, let id, let date, let title, let iconEmojiId, let topMessage, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let unreadMentionsCount, let unreadReactionsCount): + case .forumTopic(let flags, let id, let date, let title, let iconColor, let iconEmojiId, let topMessage, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let unreadMentionsCount, let unreadReactionsCount, let fromId): if boxed { - buffer.appendInt32(792599046) + buffer.appendInt32(413771876) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) + serializeInt32(iconColor, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt64(iconEmojiId!, buffer: buffer, boxed: false)} serializeInt32(topMessage, buffer: buffer, boxed: false) serializeInt32(readInboxMaxId, buffer: buffer, boxed: false) @@ -1113,14 +1114,15 @@ public extension Api { serializeInt32(unreadCount, buffer: buffer, boxed: false) serializeInt32(unreadMentionsCount, buffer: buffer, boxed: false) serializeInt32(unreadReactionsCount, buffer: buffer, boxed: false) + fromId.serialize(buffer, true) break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .forumTopic(let flags, let id, let date, let title, let iconEmojiId, let topMessage, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let unreadMentionsCount, let unreadReactionsCount): - return ("forumTopic", [("flags", String(describing: flags)), ("id", String(describing: id)), ("date", String(describing: date)), ("title", String(describing: title)), ("iconEmojiId", String(describing: iconEmojiId)), ("topMessage", String(describing: topMessage)), ("readInboxMaxId", String(describing: readInboxMaxId)), ("readOutboxMaxId", String(describing: readOutboxMaxId)), ("unreadCount", String(describing: unreadCount)), ("unreadMentionsCount", String(describing: unreadMentionsCount)), ("unreadReactionsCount", String(describing: unreadReactionsCount))]) + case .forumTopic(let flags, let id, let date, let title, let iconColor, let iconEmojiId, let topMessage, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let unreadMentionsCount, let unreadReactionsCount, let fromId): + return ("forumTopic", [("flags", String(describing: flags)), ("id", String(describing: id)), ("date", String(describing: date)), ("title", String(describing: title)), ("iconColor", String(describing: iconColor)), ("iconEmojiId", String(describing: iconEmojiId)), ("topMessage", String(describing: topMessage)), ("readInboxMaxId", String(describing: readInboxMaxId)), ("readOutboxMaxId", String(describing: readOutboxMaxId)), ("unreadCount", String(describing: unreadCount)), ("unreadMentionsCount", String(describing: unreadMentionsCount)), ("unreadReactionsCount", String(describing: unreadReactionsCount)), ("fromId", String(describing: fromId))]) } } @@ -1133,10 +1135,10 @@ public extension Api { _3 = reader.readInt32() var _4: String? _4 = parseString(reader) - var _5: Int64? - if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt64() } - var _6: Int32? - _6 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int64? + if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt64() } var _7: Int32? _7 = reader.readInt32() var _8: Int32? @@ -1147,19 +1149,27 @@ public extension Api { _10 = reader.readInt32() var _11: Int32? _11 = reader.readInt32() + var _12: Int32? + _12 = reader.readInt32() + var _13: Api.Peer? + if let signature = reader.readInt32() { + _13 = Api.parse(reader, signature: signature) as? Api.Peer + } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = _6 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil let _c9 = _9 != nil let _c10 = _10 != nil let _c11 = _11 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { - return Api.ForumTopic.forumTopic(flags: _1!, id: _2!, date: _3!, title: _4!, iconEmojiId: _5, topMessage: _6!, readInboxMaxId: _7!, readOutboxMaxId: _8!, unreadCount: _9!, unreadMentionsCount: _10!, unreadReactionsCount: _11!) + let _c12 = _12 != nil + let _c13 = _13 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 { + return Api.ForumTopic.forumTopic(flags: _1!, id: _2!, date: _3!, title: _4!, iconColor: _5!, iconEmojiId: _6, topMessage: _7!, readInboxMaxId: _8!, readOutboxMaxId: _9!, unreadCount: _10!, unreadMentionsCount: _11!, unreadReactionsCount: _12!, fromId: _13!) } else { return nil diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 5a35ab8c71..2aac2e13c8 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -201,7 +201,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEditTitle, .messageActionTopicEditIcon: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEdit: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) @@ -680,6 +680,13 @@ extension StoreMessage { } attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId)) } + } else { + switch action { + case .messageActionTopicCreate: + threadId = Int64(id) + default: + break + } } if (flags & (1 << 17)) != 0 { diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index cd63d4a567..09c5bde38f 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -87,12 +87,18 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .webViewData(text)) case let .messageActionGiftPremium(currency, amount, months): return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months)) - case let .messageActionTopicCreate(_, title, iconEmojiId): - return TelegramMediaAction(action: .topicCreated(title: title, iconFileId: iconEmojiId)) - case let .messageActionTopicEditTitle(title): - return TelegramMediaAction(action: .topicEditTitle(title: title)) - case let .messageActionTopicEditIcon(fileId): - return TelegramMediaAction(action: .topicEditIcon(fileId: fileId == 0 ? nil : fileId)) + case let .messageActionTopicCreate(_, title, iconColor, iconEmojiId): + return TelegramMediaAction(action: .topicCreated(title: title, iconColor: iconColor, iconFileId: iconEmojiId)) + case let .messageActionTopicEdit(flags, title, iconEmojiId): + var componenents: [TelegramMediaActionType.ForumTopicEditComponent] = [] + //messageActionTopicEdit#b117a9f5 flags:# title:flags.0?string icon_emoji_id:flags.1?long = MessageAction; + if let title { + componenents.append(.title(title)) + } + if (flags & (1 << 1)) != 0 { + componenents.append(.iconFileId(iconEmojiId == 0 ? nil : iconEmojiId)) + } + return TelegramMediaAction(action: .topicEdited(components: componenents)) } } diff --git a/submodules/TelegramCore/Sources/ForumChannels.swift b/submodules/TelegramCore/Sources/ForumChannels.swift index 796270697a..58fb72daef 100644 --- a/submodules/TelegramCore/Sources/ForumChannels.swift +++ b/submodules/TelegramCore/Sources/ForumChannels.swift @@ -8,29 +8,35 @@ public extension EngineMessageHistoryThread { private enum CodingKeys: String, CodingKey { case title case icon + case iconColor } public let title: String public let icon: Int64? + public let iconColor: Int32 public init( title: String, - icon: Int64? + icon: Int64?, + iconColor: Int32 ) { self.title = title self.icon = icon + self.iconColor = iconColor } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.title = try container.decode(String.self, forKey: .title) self.icon = try container.decodeIfPresent(Int64.self, forKey: .icon) + self.iconColor = try container.decodeIfPresent(Int32.self, forKey: .iconColor) ?? 0 } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.title, forKey: .title) try container.encodeIfPresent(self.icon, forKey: .icon) + try container.encode(self.iconColor, forKey: .iconColor) } public static func ==(lhs: Info, rhs: Info) -> Bool { @@ -40,24 +46,31 @@ public extension EngineMessageHistoryThread { if lhs.icon != rhs.icon { return false } + if lhs.iconColor != rhs.iconColor { + return false + } return true } } } -public struct MessageHistoryThreadData: Codable { +public struct MessageHistoryThreadData: Codable, Equatable { + public var creationDate: Int32 + public var author: PeerId public var info: EngineMessageHistoryThread.Info public var incomingUnreadCount: Int32 public var maxIncomingReadId: Int32 public var maxKnownMessageId: Int32 public var maxOutgoingReadId: Int32 + public var unreadMentionCount: Int32 + public var unreadReactionCount: Int32 } public enum CreateForumChannelTopicError { case generic } -func _internal_createForumChannelTopic(account: Account, peerId: PeerId, title: String, iconFileId: Int64?) -> Signal { +func _internal_createForumChannelTopic(account: Account, peerId: PeerId, title: String, iconColor: Int32, iconFileId: Int64?) -> Signal { return account.postbox.transaction { transaction -> Api.InputChannel? in return transaction.getPeer(peerId).flatMap(apiInputChannel) } @@ -70,10 +83,12 @@ func _internal_createForumChannelTopic(account: Account, peerId: PeerId, title: if iconFileId != nil { flags |= (1 << 3) } + flags |= (1 << 0) return account.network.request(Api.functions.channels.createForumTopic( flags: flags, channel: inputChannel, title: title, + iconColor: iconColor, iconEmojiId: iconFileId, randomId: Int64.random(in: Int64.min ..< Int64.max), sendAs: nil @@ -108,6 +123,55 @@ func _internal_createForumChannelTopic(account: Account, peerId: PeerId, title: } } +public enum EditForumChannelTopicError { + case generic +} + +func _internal_editForumChannelTopic(account: Account, peerId: PeerId, threadId: Int64, title: String, iconFileId: Int64?) -> Signal { + return account.postbox.transaction { transaction -> Api.InputChannel? in + return transaction.getPeer(peerId).flatMap(apiInputChannel) + } + |> castError(EditForumChannelTopicError.self) + |> mapToSignal { inputChannel -> Signal in + guard let inputChannel = inputChannel else { + return .fail(.generic) + } + var flags: Int32 = 0 + flags |= (1 << 0) + flags |= (1 << 1) + + return account.network.request(Api.functions.channels.editForumTopic( + flags: flags, + channel: inputChannel, + topicId: Int32(clamping: threadId), + title: title, + iconEmojiId: iconFileId ?? 0 + )) + |> mapError { _ -> EditForumChannelTopicError in + return .generic + } + |> mapToSignal { result -> Signal in + account.stateManager.addUpdates(result) + + return account.postbox.transaction { transaction -> Void in + if let initialData = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.get(MessageHistoryThreadData.self) { + var data = initialData + + data.info = EngineMessageHistoryThread.Info(title: title, icon: iconFileId, iconColor: data.info.iconColor) + + if data != initialData { + if let entry = CodableEntry(data) { + transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry) + } + } + } + } + |> castError(EditForumChannelTopicError.self) + |> ignoreValues + } + } +} + func _internal_setChannelForumMode(account: Account, peerId: PeerId, isForum: Bool) -> Signal { return account.postbox.transaction { transaction -> Api.InputChannel? in return transaction.getPeer(peerId).flatMap(apiInputChannel) @@ -193,16 +257,21 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si for topic in topics { switch topic { - case let .forumTopic(_, id, _, title, iconEmojiId, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, _, _): + case let .forumTopic(_, id, date, title, iconColor, iconEmojiId, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, fromId): let data = MessageHistoryThreadData( + creationDate: date, + author: fromId.peerId, info: EngineMessageHistoryThread.Info( title: title, - icon: iconEmojiId + icon: iconEmojiId == 0 ? nil : iconEmojiId, + iconColor: iconColor ), incomingUnreadCount: unreadCount, maxIncomingReadId: readInboxMaxId, maxKnownMessageId: topMessage, - maxOutgoingReadId: readOutboxMaxId + maxOutgoingReadId: readOutboxMaxId, + unreadMentionCount: unreadMentionsCount, + unreadReactionCount: unreadReactionsCount ) guard let info = CodableEntry(data) else { continue diff --git a/submodules/TelegramCore/Sources/SecretChats/UpdateSecretChat.swift b/submodules/TelegramCore/Sources/SecretChats/UpdateSecretChat.swift index 6e44cf5760..c4c427f30d 100644 --- a/submodules/TelegramCore/Sources/SecretChats/UpdateSecretChat.swift +++ b/submodules/TelegramCore/Sources/SecretChats/UpdateSecretChat.swift @@ -79,7 +79,7 @@ func updateSecretChat(encryptionProvider: EncryptionProvider, accountPeerId: Pee if isRemoved { let peerId = currentPeer.id - _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, threadId: nil, namespaces: .all) transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded) transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, itemId: RecentPeerItemId(peerId).rawValue) } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 20ac73dd48..0c5d856d68 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1694,17 +1694,26 @@ func resolveForumThreads(postbox: Postbox, network: Network, state: AccountMutab for topic in topics { switch topic { - case let .forumTopic(_, id, _, title, iconEmojiId, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, _, _): - state.operations.append(.ResetForumTopic(topicId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id), data: MessageHistoryThreadData( - info: EngineMessageHistoryThread.Info( - title: title, - icon: iconEmojiId + case let .forumTopic(_, id, date, title, iconColor, iconEmojiId, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, fromId): + state.operations.append(.ResetForumTopic( + topicId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id), + data: MessageHistoryThreadData( + creationDate: date, + author: fromId.peerId, + info: EngineMessageHistoryThread.Info( + title: title, + icon: iconEmojiId == 0 ? nil : iconEmojiId, + iconColor: iconColor + ), + incomingUnreadCount: unreadCount, + maxIncomingReadId: readInboxMaxId, + maxKnownMessageId: topMessage, + maxOutgoingReadId: readOutboxMaxId, + unreadMentionCount: unreadMentionsCount, + unreadReactionCount: unreadReactionsCount ), - incomingUnreadCount: unreadCount, - maxIncomingReadId: readInboxMaxId, - maxKnownMessageId: topMessage, - maxOutgoingReadId: readOutboxMaxId - ), pts: pts)) + pts: pts + )) } } } @@ -1786,16 +1795,21 @@ func resolveForumThreads(postbox: Postbox, network: Network, fetchedChatList: Fe for topic in topics { switch topic { - case let .forumTopic(_, id, _, title, iconEmojiId, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, _, _): + case let .forumTopic(_, id, date, title, iconColor, iconEmojiId, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, fromId): fetchedChatList.threadInfos[MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id)] = MessageHistoryThreadData( + creationDate: date, + author: fromId.peerId, info: EngineMessageHistoryThread.Info( title: title, - icon: iconEmojiId + icon: iconEmojiId == 0 ? nil : iconEmojiId, + iconColor: iconColor ), incomingUnreadCount: unreadCount, maxIncomingReadId: readInboxMaxId, maxKnownMessageId: topMessage, - maxOutgoingReadId: readOutboxMaxId + maxOutgoingReadId: readOutboxMaxId, + unreadMentionCount: unreadMentionsCount, + unreadReactionCount: unreadReactionsCount ) } } @@ -2864,18 +2878,23 @@ func replayFinalState( for media in message.media { if let action = media as? TelegramMediaAction { switch action.action { - case let .topicEditTitle(title): - if var data = transaction.getMessageHistoryThreadInfo(peerId: id.peerId, threadId: threadId)?.get(MessageHistoryThreadData.self) { - data.info = EngineMessageHistoryThread.Info(title: title, icon: data.info.icon) - if let entry = CodableEntry(data) { - transaction.setMessageHistoryThreadInfo(peerId: id.peerId, threadId: threadId, info: entry) + case let .topicEdited(components): + if let initialData = transaction.getMessageHistoryThreadInfo(peerId: id.peerId, threadId: threadId)?.get(MessageHistoryThreadData.self) { + var data = initialData + + for component in components { + switch component { + case let .title(title): + data.info = EngineMessageHistoryThread.Info(title: title, icon: data.info.icon, iconColor: data.info.iconColor) + case let .iconFileId(fileId): + data.info = EngineMessageHistoryThread.Info(title: data.info.title, icon: fileId == 0 ? nil : fileId, iconColor: data.info.iconColor) + } } - } - case let .topicEditIcon(fileId): - if var data = transaction.getMessageHistoryThreadInfo(peerId: id.peerId, threadId: threadId)?.get(MessageHistoryThreadData.self) { - data.info = EngineMessageHistoryThread.Info(title: data.info.title, icon: fileId == 0 ? nil : fileId) - if let entry = CodableEntry(data) { - transaction.setMessageHistoryThreadInfo(peerId: id.peerId, threadId: threadId, info: entry) + + if data != initialData { + if let entry = CodableEntry(data) { + transaction.setMessageHistoryThreadInfo(peerId: id.peerId, threadId: threadId, info: entry) + } } } default: diff --git a/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift b/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift index f2432eb309..58b9bd40ed 100644 --- a/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift +++ b/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift @@ -8,7 +8,7 @@ func cloudChatAddRemoveChatOperation(transaction: Transaction, peerId: PeerId, r transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatRemoveChatOperation(peerId: peerId, reportChatSpam: reportChatSpam, deleteGloballyIfPossible: deleteGloballyIfPossible, topMessageId: transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud))) } -func cloudChatAddClearHistoryOperation(transaction: Transaction, peerId: PeerId, explicitTopMessageId: MessageId?, minTimestamp: Int32?, maxTimestamp: Int32?, type: CloudChatClearHistoryType) { +func cloudChatAddClearHistoryOperation(transaction: Transaction, peerId: PeerId, threadId: Int64?, explicitTopMessageId: MessageId?, minTimestamp: Int32?, maxTimestamp: Int32?, type: CloudChatClearHistoryType) { if type == .scheduledMessages { var messageIds: [MessageId] = [] transaction.withAllMessages(peerId: peerId, namespace: Namespaces.Message.ScheduledCloud) { message -> Bool in @@ -24,9 +24,9 @@ func cloudChatAddClearHistoryOperation(transaction: Transaction, peerId: PeerId, topMessageId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) } if let topMessageId = topMessageId { - transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: topMessageId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type)) + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: topMessageId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type)) } else if case .forEveryone = type { - transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: .max), minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type)) + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: .max), threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type)) } } } diff --git a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift index cb12d5ffe1..8e7963bf1e 100644 --- a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift @@ -310,7 +310,7 @@ private func removeChat(transaction: Transaction, postbox: Postbox, network: Net |> then(deleteUser) |> then(reportSignal) |> then(postbox.transaction { transaction -> Void in - _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, threadId: nil, namespaces: .all) }) } else if peer.id.namespace == Namespaces.Peer.CloudUser { if let inputPeer = apiInputPeer(peer) { @@ -329,7 +329,7 @@ private func removeChat(transaction: Transaction, postbox: Postbox, network: Net return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId?.id ?? Int32.max - 1, justClear: false, minTimestamp: nil, maxTimestamp: nil, type: operation.deleteGloballyIfPossible ? .forEveryone : .forLocalPeer) |> then(reportSignal) |> then(postbox.transaction { transaction -> Void in - _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, threadId: nil, namespaces: .not(Namespaces.Message.allScheduled)) }) } else { return .complete() @@ -395,20 +395,33 @@ private func _internal_clearHistory(transaction: Transaction, postbox: Postbox, if operation.minTimestamp != nil { return .complete() } else { - var flags: Int32 = 0 - if operation.type == .forEveryone { - flags |= 1 << 0 - } - return network.request(Api.functions.channels.deleteHistory(flags: flags, channel: inputChannel, maxId: operation.topMessageId.id)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { updates -> Signal in - if let updates = updates { - stateManager.addUpdates(updates) + if let threadId = operation.threadId { + return network.request(Api.functions.channels.deleteTopicHistory(channel: inputChannel, topMsgId: Int32(clamping: threadId))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + if let _ = result { + } + return .complete() + } + } else { + var flags: Int32 = 0 + if operation.type == .forEveryone { + flags |= 1 << 0 + } + return network.request(Api.functions.channels.deleteHistory(flags: flags, channel: inputChannel, maxId: operation.topMessageId.id)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + stateManager.addUpdates(updates) + } + return .complete() } - return .complete() } } } else { diff --git a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift index 1813859461..b1e7024af0 100644 --- a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift @@ -298,7 +298,7 @@ func processSecretChatIncomingDecryptedOperations(encryptionProvider: Encryption _internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: filteredMessageIds) } case .clearHistory: - _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, threadId: nil, namespaces: .all) case let .markMessagesContentAsConsumed(globallyUniqueIds): var messageIds: [MessageId] = [] for id in globallyUniqueIds { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift index 79e7c58533..40d830bf1b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift @@ -112,13 +112,15 @@ public extension CloudChatClearHistoryType { public final class CloudChatClearHistoryOperation: PostboxCoding { public let peerId: PeerId public let topMessageId: MessageId + public let threadId: Int64? public let minTimestamp: Int32? public let maxTimestamp: Int32? public let type: CloudChatClearHistoryType - public init(peerId: PeerId, topMessageId: MessageId, minTimestamp: Int32?, maxTimestamp: Int32?, type: CloudChatClearHistoryType) { + public init(peerId: PeerId, topMessageId: MessageId, threadId: Int64?, minTimestamp: Int32?, maxTimestamp: Int32?, type: CloudChatClearHistoryType) { self.peerId = peerId self.topMessageId = topMessageId + self.threadId = threadId self.minTimestamp = minTimestamp self.maxTimestamp = maxTimestamp self.type = type @@ -127,6 +129,7 @@ public final class CloudChatClearHistoryOperation: PostboxCoding { public init(decoder: PostboxDecoder) { self.peerId = PeerId(decoder.decodeInt64ForKey("p", orElse: 0)) self.topMessageId = MessageId(peerId: PeerId(decoder.decodeInt64ForKey("m.p", orElse: 0)), namespace: decoder.decodeInt32ForKey("m.n", orElse: 0), id: decoder.decodeInt32ForKey("m.i", orElse: 0)) + self.threadId = decoder.decodeOptionalInt64ForKey("threadId") self.minTimestamp = decoder.decodeOptionalInt32ForKey("minTimestamp") self.maxTimestamp = decoder.decodeOptionalInt32ForKey("maxTimestamp") self.type = CloudChatClearHistoryType(rawValue: decoder.decodeInt32ForKey("type", orElse: 0)) ?? .forLocalPeer @@ -137,6 +140,11 @@ public final class CloudChatClearHistoryOperation: PostboxCoding { encoder.encodeInt64(self.topMessageId.peerId.toInt64(), forKey: "m.p") encoder.encodeInt32(self.topMessageId.namespace, forKey: "m.n") encoder.encodeInt32(self.topMessageId.id, forKey: "m.i") + if let threadId = self.threadId { + encoder.encodeInt64(threadId, forKey: "threadId") + } else { + encoder.encodeNil(forKey: "threadId") + } if let minTimestamp = self.minTimestamp { encoder.encodeInt32(minTimestamp, forKey: "minTimestamp") } else { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift index eecc2427fa..6200d78e90 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift @@ -65,6 +65,9 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = { case .broadcast: return .channel case .group: + if channel.flags.contains(.isForum) { + return [] + } if channel.username != nil { return .group } else { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index e7e371272f..34f05f42ef 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -24,6 +24,38 @@ public enum SentSecureValueType: Int32 { } public enum TelegramMediaActionType: PostboxCoding, Equatable { + public enum ForumTopicEditComponent: PostboxCoding, Equatable { + case title(String) + case iconFileId(Int64?) + + public init(decoder: PostboxDecoder) { + switch decoder.decodeInt32ForKey("_t", orElse: 0) { + case 0: + self = .title(decoder.decodeStringForKey("title", orElse: "")) + case 1: + self = .iconFileId(decoder.decodeOptionalInt64ForKey("fileId")) + default: + assertionFailure() + self = .title("") + } + } + + public func encode(_ encoder: PostboxEncoder) { + switch self { + case let .title(title): + encoder.encodeInt32(0, forKey: "_t") + encoder.encodeString(title, forKey: "title") + case let .iconFileId(fileId): + encoder.encodeInt32(1, forKey: "_t") + if let fileId = fileId { + encoder.encodeInt64(fileId, forKey: "fileId") + } else { + encoder.encodeNil(forKey: "fileId") + } + } + } + } + case unknown case groupCreated(title: String) case addedMembers(peerIds: [PeerId]) @@ -52,9 +84,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case joinedByRequest case webViewData(String) case giftPremium(currency: String, amount: Int64, months: Int32) - case topicCreated(title: String, iconFileId: Int64?) - case topicEditTitle(title: String) - case topicEditIcon(fileId: Int64?) + case topicCreated(title: String, iconColor: Int32, iconFileId: Int64?) + case topicEdited(components: [ForumTopicEditComponent]) public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -126,11 +157,9 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 27: self = .giftPremium(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), months: decoder.decodeInt32ForKey("months", orElse: 0)) case 28: - self = .topicCreated(title: decoder.decodeStringForKey("t", orElse: ""), iconFileId: decoder.decodeOptionalInt64ForKey("fid")) + self = .topicCreated(title: decoder.decodeStringForKey("title", orElse: ""), iconColor: decoder.decodeInt32ForKey("iconColor", orElse: 0), iconFileId: decoder.decodeOptionalInt64ForKey("iconFileId")) case 29: - self = .topicEditTitle(title: decoder.decodeStringForKey("t", orElse: "")) - case 30: - self = .topicEditIcon(fileId: decoder.decodeOptionalInt64ForKey("fid")) + self = .topicEdited(components: decoder.decodeObjectArrayWithDecoderForKey("components")) default: self = .unknown } @@ -260,24 +289,18 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encodeString(currency, forKey: "currency") encoder.encodeInt64(amount, forKey: "amount") encoder.encodeInt32(months, forKey: "months") - case let .topicCreated(title, iconFileId): + case let .topicCreated(title, iconColor, iconFileId): encoder.encodeInt32(28, forKey: "_rawValue") - encoder.encodeString(title, forKey: "t") - if let fileId = iconFileId { - encoder.encodeInt64(fileId, forKey: "fid") + encoder.encodeString(title, forKey: "title") + encoder.encodeInt32(iconColor, forKey: "iconColor") + if let iconFileId = iconFileId { + encoder.encodeInt64(iconFileId, forKey: "iconFileId") } else { - encoder.encodeNil(forKey: "fid") + encoder.encodeNil(forKey: "iconFileId") } - case let .topicEditTitle(title): + case let .topicEdited(components): encoder.encodeInt32(29, forKey: "_rawValue") - encoder.encodeString(title, forKey: "t") - case let .topicEditIcon(fileId): - encoder.encodeInt32(30, forKey: "_rawValue") - if let fileId = fileId { - encoder.encodeInt64(fileId, forKey: "fid") - } else { - encoder.encodeNil(forKey: "fid") - } + encoder.encodeObjectArray(components, forKey: "components") } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift index 7ae60b1296..6560a519bf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift @@ -388,11 +388,17 @@ extension EngineChatList.Item { forumTopicTitle = forumTopicData.info.title } + var readCounters = readState.flatMap(EnginePeerReadCounters.init) + + if let channel = renderedPeer.peer as? TelegramChannel, channel.flags.contains(.isForum) { + readCounters = nil + } + self.init( id: .chatList(index.messageIndex.id.peerId), index: .chatList(index), messages: messages.map(EngineMessage.init), - readCounters: readState.flatMap(EnginePeerReadCounters.init), + readCounters: readCounters, isMuted: isRemovedFromTotalUnreadCount, draft: draft, threadInfo: nil, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift index 6f256e3306..d94ad48c48 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift @@ -76,7 +76,7 @@ func _internal_deleteAllMessagesWithForwardAuthor(transaction: Transaction, medi } } -func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, namespaces: MessageIdNamespaces) { +func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, threadId: Int64?, namespaces: MessageIdNamespaces) { if peerId.namespace == Namespaces.Peer.SecretChat { var resourceIds: [MediaResourceId] = [] transaction.withAllMessages(peerId: peerId, { message in @@ -87,11 +87,11 @@ func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() } } - transaction.clearHistory(peerId, minTimestamp: nil, maxTimestamp: nil, namespaces: namespaces, forEachMedia: { _ in + transaction.clearHistory(peerId, threadId: threadId, minTimestamp: nil, maxTimestamp: nil, namespaces: namespaces, forEachMedia: { _ in }) } -func _internal_clearHistoryInRange(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, minTimestamp: Int32, maxTimestamp: Int32, namespaces: MessageIdNamespaces) { +func _internal_clearHistoryInRange(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, threadId: Int64?, minTimestamp: Int32, maxTimestamp: Int32, namespaces: MessageIdNamespaces) { if peerId.namespace == Namespaces.Peer.SecretChat { var resourceIds: [MediaResourceId] = [] transaction.withAllMessages(peerId: peerId, { message in @@ -104,7 +104,7 @@ func _internal_clearHistoryInRange(transaction: Transaction, mediaBox: MediaBox, let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() } } - transaction.clearHistory(peerId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, forEachMedia: { _ in + transaction.clearHistory(peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, forEachMedia: { _ in }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift index 29d426d335..18a38af631 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift @@ -96,13 +96,13 @@ func deleteMessagesInteractively(transaction: Transaction, stateManager: Account } } -func _internal_clearHistoryInRangeInteractively(postbox: Postbox, peerId: PeerId, minTimestamp: Int32, maxTimestamp: Int32, type: InteractiveHistoryClearingType) -> Signal { +func _internal_clearHistoryInRangeInteractively(postbox: Postbox, peerId: PeerId, threadId: Int64?, minTimestamp: Int32, maxTimestamp: Int32, type: InteractiveHistoryClearingType) -> Signal { return postbox.transaction { transaction -> Void in if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel { - cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, explicitTopMessageId: nil, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: CloudChatClearHistoryType(type)) + cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, threadId: threadId, explicitTopMessageId: nil, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: CloudChatClearHistoryType(type)) if type == .scheduledMessages { } else { - _internal_clearHistoryInRange(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistoryInRange(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: .not(Namespaces.Message.allScheduled)) } } else if peerId.namespace == Namespaces.Peer.SecretChat { /*_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .all) @@ -129,22 +129,22 @@ func _internal_clearHistoryInRangeInteractively(postbox: Postbox, peerId: PeerId } } -func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, type: InteractiveHistoryClearingType) -> Signal { +func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, threadId: Int64?, type: InteractiveHistoryClearingType) -> Signal { return postbox.transaction { transaction -> Void in if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel { - cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, explicitTopMessageId: nil, minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(type)) + cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, threadId: threadId, explicitTopMessageId: nil, minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(type)) if type == .scheduledMessages { - _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .just(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, namespaces: .just(Namespaces.Message.allScheduled)) } else { var topIndex: MessageIndex? if let topMessageId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud), let topMessage = transaction.getMessage(topMessageId) { topIndex = topMessage.index } - _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, namespaces: .not(Namespaces.Message.allScheduled)) if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, let migrationReference = cachedData.migrationReference { - cloudChatAddClearHistoryOperation(transaction: transaction, peerId: migrationReference.maxMessageId.peerId, explicitTopMessageId: MessageId(peerId: migrationReference.maxMessageId.peerId, namespace: migrationReference.maxMessageId.namespace, id: migrationReference.maxMessageId.id + 1), minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(type)) - _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: migrationReference.maxMessageId.peerId, namespaces: .all) + cloudChatAddClearHistoryOperation(transaction: transaction, peerId: migrationReference.maxMessageId.peerId, threadId: threadId, explicitTopMessageId: MessageId(peerId: migrationReference.maxMessageId.peerId, namespace: migrationReference.maxMessageId.namespace, id: migrationReference.maxMessageId.id + 1), minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(type)) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: migrationReference.maxMessageId.peerId, threadId: threadId, namespaces: .all) } if let topIndex = topIndex { if peerId.namespace == Namespaces.Peer.CloudUser { @@ -155,7 +155,7 @@ func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, type: } } } else if peerId.namespace == Namespaces.Peer.SecretChat { - _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: nil, namespaces: .all) if let state = transaction.getPeerChatState(peerId) as? SecretChatState { var layer: SecretChatLayer? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift index 643d238f1f..28014193fa 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift @@ -7,6 +7,7 @@ public final class SparseMessageList { private let queue: Queue private let account: Account private let peerId: PeerId + private let threadId: Int64? private let messageTag: MessageTags private struct TopSection: Equatable { @@ -91,76 +92,79 @@ public final class SparseMessageList { let statePromise = Promise() - init(queue: Queue, account: Account, peerId: PeerId, messageTag: MessageTags) { + init(queue: Queue, account: Account, peerId: PeerId, threadId: Int64?, messageTag: MessageTags) { self.queue = queue self.account = account self.peerId = peerId + self.threadId = threadId self.messageTag = messageTag self.resetTopSection() - self.sparseItemsDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) - } - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { - return .single(SparseItems(items: [])) + if self.threadId == nil { + self.sparseItemsDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) } - guard let messageFilter = messageFilterForTagMask(messageTag) else { - return .single(SparseItems(items: [])) - } - return account.network.request(Api.functions.messages.getSearchResultsPositions(peer: inputPeer, filter: messageFilter, offsetId: 0, limit: 1000)) - |> map { result -> SparseItems in - switch result { - case let .searchResultsPositions(totalCount, positions): - struct Position: Equatable { - var id: Int32 - var date: Int32 - var offset: Int - } - var positions: [Position] = positions.map { position -> Position in - switch position { - case let .searchResultPosition(id, date, offset): - return Position(id: id, date: date, offset: Int(offset)) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .single(SparseItems(items: [])) + } + guard let messageFilter = messageFilterForTagMask(messageTag) else { + return .single(SparseItems(items: [])) + } + return account.network.request(Api.functions.messages.getSearchResultsPositions(peer: inputPeer, filter: messageFilter, offsetId: 0, limit: 1000)) + |> map { result -> SparseItems in + switch result { + case let .searchResultsPositions(totalCount, positions): + struct Position: Equatable { + var id: Int32 + var date: Int32 + var offset: Int } - } - positions.sort(by: { lhs, rhs in - return lhs.id > rhs.id - }) - - var result = SparseItems(items: []) - for i in 0 ..< positions.count { - if i != 0 { - let deltaCount = positions[i].offset - 1 - positions[i - 1].offset - if deltaCount > 0 { - result.items.append(.range(count: deltaCount)) + var positions: [Position] = positions.map { position -> Position in + switch position { + case let .searchResultPosition(id, date, offset): + return Position(id: id, date: date, offset: Int(offset)) } } - result.items.append(.anchor(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: positions[i].id), timestamp: positions[i].date, message: nil)) - if i == positions.count - 1 { - let remainingCount = Int(totalCount) - 1 - positions[i].offset - if remainingCount > 0 { - result.items.append(.range(count: remainingCount)) + positions.sort(by: { lhs, rhs in + return lhs.id > rhs.id + }) + + var result = SparseItems(items: []) + for i in 0 ..< positions.count { + if i != 0 { + let deltaCount = positions[i].offset - 1 - positions[i - 1].offset + if deltaCount > 0 { + result.items.append(.range(count: deltaCount)) + } + } + result.items.append(.anchor(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: positions[i].id), timestamp: positions[i].date, message: nil)) + if i == positions.count - 1 { + let remainingCount = Int(totalCount) - 1 - positions[i].offset + if remainingCount > 0 { + result.items.append(.range(count: remainingCount)) + } } } + + return result } - - return result + } + |> `catch` { _ -> Signal in + return .single(SparseItems(items: [])) } } - |> `catch` { _ -> Signal in - return .single(SparseItems(items: [])) - } + |> deliverOn(self.queue)).start(next: { [weak self] sparseItems in + guard let strongSelf = self else { + return + } + strongSelf.sparseItems = sparseItems + if strongSelf.topSection != nil { + strongSelf.updateState() + } + }) } - |> deliverOn(self.queue)).start(next: { [weak self] sparseItems in - guard let strongSelf = self else { - return - } - strongSelf.sparseItems = sparseItems - if strongSelf.topSection != nil { - strongSelf.updateState() - } - }) self.deletedMessagesDisposable = (account.postbox.combinedView(keys: [.deletedMessages(peerId: peerId)]) |> deliverOn(self.queue)).start(next: { [weak self] views in @@ -183,12 +187,16 @@ public final class SparseMessageList { private func resetTopSection() { let count: Int - /*#if DEBUG - count = 20 - #else*/ count = 200 - //#endif - self.topItemsDisposable.set((self.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: peerId), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: count, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tagMask: self.messageTag, appendMessagesFromTheSameGroup: false, namespaces: .not(Set(Namespaces.Message.allScheduled)), orderStatistics: []) + + let location: ChatLocationInput + if let threadId = self.threadId { + location = .thread(peerId: self.peerId, threadId: threadId, data: .single(MessageHistoryViewExternalInput(content: .thread(peerId: self.peerId, id: threadId, holes: [:]), maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil))) + } else { + location = .peer(peerId: self.peerId) + } + + self.topItemsDisposable.set((self.account.postbox.aroundMessageHistoryViewForLocation(location, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: count, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tagMask: self.messageTag, appendMessagesFromTheSameGroup: false, namespaces: .not(Set(Namespaces.Message.allScheduled)), orderStatistics: []) |> deliverOn(self.queue)).start(next: { [weak self] view, updateType, _ in guard let strongSelf = self else { return @@ -688,11 +696,11 @@ public final class SparseMessageList { } } - init(account: Account, peerId: PeerId, messageTag: MessageTags) { + init(account: Account, peerId: PeerId, threadId: Int64?, messageTag: MessageTags) { self.queue = Queue() let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, account: account, peerId: peerId, messageTag: messageTag) + return Impl(queue: queue, account: account, peerId: peerId, threadId: threadId, messageTag: messageTag) }) } @@ -878,7 +886,7 @@ public final class SparseMessageCalendar { self.statePromise.set(.single(self.state)) - return _internal_clearHistoryInRangeInteractively(postbox: self.account.postbox, peerId: self.peerId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type).start(completed: { + return _internal_clearHistoryInRangeInteractively(postbox: self.account.postbox, peerId: self.peerId, threadId: nil, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type).start(completed: { completion() }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index e3cfc381d5..24f9e6dbc6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -77,8 +77,8 @@ public extension TelegramEngine { return _internal_deleteMessagesInteractively(account: self.account, messageIds: messageIds, type: type, deleteAllInGroup: deleteAllInGroup) } - public func clearHistoryInteractively(peerId: PeerId, type: InteractiveHistoryClearingType) -> Signal { - return _internal_clearHistoryInteractively(postbox: self.account.postbox, peerId: peerId, type: type) + public func clearHistoryInteractively(peerId: PeerId, threadId: Int64?, type: InteractiveHistoryClearingType) -> Signal { + return _internal_clearHistoryInteractively(postbox: self.account.postbox, peerId: peerId, threadId: threadId, type: type) } public func clearAuthorHistory(peerId: PeerId, memberId: PeerId) -> Signal { @@ -285,8 +285,8 @@ public extension TelegramEngine { } } - public func sparseMessageList(peerId: EnginePeer.Id, tag: EngineMessage.Tags) -> SparseMessageList { - return SparseMessageList(account: self.account, peerId: peerId, messageTag: tag) + public func sparseMessageList(peerId: EnginePeer.Id, threadId: Int64?, tag: EngineMessage.Tags) -> SparseMessageList { + return SparseMessageList(account: self.account, peerId: peerId, threadId: threadId, messageTag: tag) } public func sparseMessageCalendar(peerId: EnginePeer.Id, tag: EngineMessage.Tags) -> SparseMessageCalendar { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerChat.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerChat.swift index cbaeba1c0c..f9f556dca3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerChat.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerChat.swift @@ -58,17 +58,17 @@ func _internal_removePeerChat(account: Account, transaction: Transaction, mediaB } } } - _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, threadId: nil, namespaces: .all) transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded) transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, itemId: RecentPeerItemId(peerId).rawValue) } else { cloudChatAddRemoveChatOperation(transaction: transaction, peerId: peerId, reportChatSpam: reportChatSpam, deleteGloballyIfPossible: deleteGloballyIfPossible) if peerId.namespace == Namespaces.Peer.CloudUser { transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded) - _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, threadId: nil, namespaces: .all) } else if peerId.namespace == Namespaces.Peer.CloudGroup { transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded) - _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, threadId: nil, namespaces: .all) } else { transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 9ff693db20..6cefc2cc13 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -802,8 +802,23 @@ public extension TelegramEngine { return _internal_setChannelForumMode(account: self.account, peerId: id, isForum: isForum) } - public func createForumChannelTopic(id: EnginePeer.Id, title: String, iconFileId: Int64?) -> Signal { - return _internal_createForumChannelTopic(account: self.account, peerId: id, title: title, iconFileId: iconFileId) + public func createForumChannelTopic(id: EnginePeer.Id, title: String, iconColor: Int32, iconFileId: Int64?) -> Signal { + return _internal_createForumChannelTopic(account: self.account, peerId: id, title: title, iconColor: iconColor, iconFileId: iconFileId) + } + + public func editForumChannelTopic(id: EnginePeer.Id, threadId: Int64, title: String, iconFileId: Int64?) -> Signal { + return _internal_editForumChannelTopic(account: self.account, peerId: id, threadId: threadId, title: title, iconFileId: iconFileId) + } + + public func removeForumChannelThread(id: EnginePeer.Id, threadId: Int64) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + cloudChatAddClearHistoryOperation(transaction: transaction, peerId: id, threadId: threadId, explicitTopMessageId: nil, minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(.forEveryone)) + + transaction.setMessageHistoryThreadInfo(peerId: id, threadId: threadId, info: nil) + + _internal_clearHistory(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: id, threadId: threadId, namespaces: .not(Namespaces.Message.allScheduled)) + } + |> ignoreValues } } } diff --git a/submodules/TelegramCore/Sources/UpdatePeers.swift b/submodules/TelegramCore/Sources/UpdatePeers.swift index 27c3271085..a89c273305 100644 --- a/submodules/TelegramCore/Sources/UpdatePeers.swift +++ b/submodules/TelegramCore/Sources/UpdatePeers.swift @@ -73,7 +73,7 @@ public func updatePeers(transaction: Transaction, peers: [Peer], update: (Peer?, if let channel = updated as? TelegramChannel { switch channel.participationStatus { case .member: - updatePeerChatInclusionWithMinTimestamp(transaction: transaction, id: peerId, minTimestamp: channel.creationDate, forceRootGroupIfNotExists: false) + updatePeerChatInclusionWithMinTimestamp(transaction: transaction, id: peerId, minTimestamp: channel.creationDate, forceRootGroupIfNotExists: true) case .left: transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded) case .kicked where channel.creationDate == 0: diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 534b2a7727..aa7f0e7d11 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -682,13 +682,29 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, case .topicCreated: //TODO:localize attributedString = NSAttributedString(string: "Topic created", font: titleFont, textColor: primaryTextColor) - case let .topicEditTitle(title): + case let .topicEdited(components): //TODO:localize - attributedString = NSAttributedString(string: "Topic renamed to \"\(title)\"", font: titleFont, textColor: primaryTextColor) - case let .topicEditIcon(fileId): - let _ = fileId - //TODO:localize - attributedString = NSAttributedString(string: "Topic icon changed", font: titleFont, textColor: primaryTextColor) + if let title = components.compactMap({ item -> String? in + switch item { + case let .title(title): + return title + default: + return nil + } + }).first { + attributedString = NSAttributedString(string: "Topic renamed to \"\(title)\"", font: titleFont, textColor: primaryTextColor) + } else if let maybeFileId = components.compactMap({ item -> Int64? in + switch item { + case let .iconFileId(id): + return id ?? 0 + default: + return nil + } + }).first { + let _ = maybeFileId + //TODO:localize + attributedString = NSAttributedString(string: "Topic icon changed", font: titleFont, textColor: primaryTextColor) + } case .unknown: attributedString = nil } diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index d406f8bebf..9ca85762fd 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -227,7 +227,7 @@ public final class EmojiStatusComponent: Component { func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor]) -> UIImage? { return generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) - + context.saveGState() let scale: CGFloat = size.width / 32.0 diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index c45cf5b165..8434f4f60d 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -357,9 +357,9 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { case .create: self.title = "" self.fileId = 0 - case let .edit(topic): - self.title = topic.info.title - self.fileId = topic.info.icon ?? 0 + case let .edit(info): + self.title = info.title + self.fileId = info.icon ?? 0 } super.init() @@ -670,7 +670,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { public class ForumCreateTopicScreen: ViewControllerComponentContainer { public enum Mode: Equatable { case create - case edit(topic: ForumChannelTopics.Item) + case edit(topic: EngineMessageHistoryThread.Info) } private var state: (String, Int64?) = ("", nil) @@ -685,6 +685,7 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { iconUpdatedImpl?(fileId) }), navigationBarAppearance: .transparent) + //TODO:localize let title: String let doneTitle: String switch mode { @@ -704,6 +705,10 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: doneTitle, style: .done, target: self, action: #selector(self.createPressed)) self.navigationItem.rightBarButtonItem?.isEnabled = false + if case .edit = mode { + self.navigationItem.rightBarButtonItem?.isEnabled = true + } + titleUpdatedImpl = { [weak self] title in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index d13a574f74..3217b9533f 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4809,14 +4809,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - if let threadInfo = messageAndTopic.threadInfo, let message = message { + if let threadInfo = messageAndTopic.threadInfo { strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, customTitle: threadInfo.title, onlineMemberCount: onlineMemberCount, isScheduledMessages: false) let avatarContent: EmojiStatusComponent.Content if let fileId = threadInfo.icon { avatarContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: strongSelf.presentationData.theme.list.mediaPlaceholderColor, themeColor: nil, loopMode: .count(2)) } else { - avatarContent = .topic(title: String(threadInfo.title.prefix(1)), colorIndex: Int(message.id.id), size: CGSize(width: 32.0, height: 32.0)) + avatarContent = .topic(title: String(threadInfo.title.prefix(1)), colorIndex: Int(threadInfo.iconColor), size: CGSize(width: 32.0, height: 32.0)) } (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setStatus(context: strongSelf.context, content: avatarContent) } else { @@ -9979,7 +9979,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .peer(peerId) = self.chatLocation { let context = self.context - self.keepPeerInfoScreenDataHotDisposable.set(keepPeerInfoScreenDataHot(context: context, peerId: peerId).start()) + self.keepPeerInfoScreenDataHotDisposable.set(keepPeerInfoScreenDataHot(context: context, peerId: peerId, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder).start()) if peerId.namespace == Namespaces.Peer.CloudUser { self.preloadAvatarDisposable.set((peerInfoProfilePhotosWithCache(context: context, peerId: peerId) @@ -10858,7 +10858,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } if value == .commit { - let _ = strongSelf.context.engine.messages.clearHistoryInteractively(peerId: peerId, type: type).start(completed: { + let _ = strongSelf.context.engine.messages.clearHistoryInteractively(peerId: peerId, threadId: nil, type: type).start(completed: { self?.chatDisplayNode.historyNode.historyAppearsCleared = false }) return true @@ -11140,7 +11140,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })) case .replyThread: - break + if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum), case let .replyThread(message) = self.chatLocation { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: channel, mode: .forumTopic(thread: message), avatarInitiallyExpanded: false, fromChat: true, requestsContext: self.inviteRequestsContext) { + self.effectiveNavigationController?.pushViewController(infoController) + } + } case .feed: break } @@ -15682,7 +15686,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self?.playShakeAnimation() } } else if let navigationController = strongSelf.effectiveNavigationController { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: peerId.id), subject: subject, keepStack: .always, peekData: peekData)) + if case let .channel(channel) = peerId, channel.flags.contains(.isForum) { + strongSelf.context.sharedContext.navigateToForumChannel(context: strongSelf.context, peerId: peerId.id, navigationController: navigationController) + } else { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: peerId.id), subject: subject, keepStack: .always, peekData: peekData)) + } } case .info: strongSelf.navigationActionDisposable.set((strongSelf.context.account.postbox.loadedPeerWithId(peerId.id) diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 7e92fa3c9b..2fbeddb5d8 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -202,7 +202,7 @@ func chatHistoryEntriesForView( } var addedThreadHead = false - if case let .replyThread(replyThreadMessage) = location, view.earlierId == nil, !view.holeEarlier, !view.isLoading { + if case let .replyThread(replyThreadMessage) = location, !replyThreadMessage.isForumPost, view.earlierId == nil, !view.holeEarlier, !view.isLoading { loop: for entry in view.additionalData { switch entry { case let .message(id, messages) where id == replyThreadMessage.effectiveTopId: diff --git a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift index fb9bf5ab34..afebcc3bab 100644 --- a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift @@ -564,19 +564,24 @@ final class ChatQrCodeScreen: ViewController { static let themeCrossfadeDelay: Double = 0.05 enum Subject { - case peer(Peer) + case peer(peer: Peer, threadId: Int64?) case messages([Message]) var fileName: String { switch self { - case let .peer(peer): + case let .peer(peer, threadId): + var result: String if let addressName = peer.addressName, !addressName.isEmpty { - return "t_me-\(peer.addressName ?? "")" + result = "t_me-\(peer.addressName ?? "")" } else if let peer = peer as? TelegramUser { - return "t_me-\(peer.phone ?? "")" + result = "t_me-\(peer.phone ?? "")" } else { - return "t_me-\(Int32.random(in: 0 ..< Int32.max))" + result = "t_me-\(Int32.random(in: 0 ..< Int32.max))" } + if let threadId = threadId { + result.append("-\(threadId)") + } + return result case let .messages(messages): if let message = messages.first, let chatPeer = message.peers[message.id.peerId] as? TelegramChannel, message.id.namespace == Namespaces.Message.Cloud, let addressName = chatPeer.addressName, !addressName.isEmpty { return "t_me-\(addressName)-\(message.id.id)" @@ -800,8 +805,8 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg self.wrappingScrollNode.view.canCancelContentTouches = true switch controller.subject { - case let .peer(peer): - self.contentNode = QrContentNode(context: context, peer: peer, isStatic: false) + case let .peer(peer, threadId): + self.contentNode = QrContentNode(context: context, peer: peer, threadId: threadId, isStatic: false) case let .messages(messages): self.contentNode = MessageContentNode(context: context, messages: messages) } @@ -994,7 +999,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg let initiallySelectedEmoticon: Signal let sharedData = self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings]) |> take(1) - if case let .peer(peer) = controller.subject, peer.id != self.context.account.peerId { + if case let .peer(peer, _) = controller.subject, peer.id != self.context.account.peerId { let themeEmoticon = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ThemeEmoticon(id: peer.id)) initiallySelectedEmoticon = combineLatest(themeEmoticon, sharedData) |> map { themeEmoticon, sharedData -> String in @@ -1434,6 +1439,7 @@ private protocol ContentNode: ASDisplayNode { private class QrContentNode: ASDisplayNode, ContentNode { private let context: AccountContext private let peer: Peer + private let threadId: Int64? private let isStatic: Bool fileprivate let containerNode: ASDisplayNode @@ -1463,9 +1469,10 @@ private class QrContentNode: ASDisplayNode, ContentNode { return false } - init(context: AccountContext, peer: Peer, isStatic: Bool = false) { + init(context: AccountContext, peer: Peer, threadId: Int64?, isStatic: Bool = false) { self.context = context self.peer = peer + self.threadId = threadId self.isStatic = isStatic self.containerNode = ASDisplayNode() @@ -1506,12 +1513,15 @@ private class QrContentNode: ASDisplayNode, ContentNode { self.codeStaticIconNode = nil } - let codeText: String + var codeText: String if let addressName = peer.addressName, !addressName.isEmpty { codeText = "@\(peer.addressName ?? "")".uppercased() } else { codeText = peer.debugDisplayTitle.uppercased() } + if let threadId = self.threadId { + codeText += "/\(threadId)" + } self.codeTextNode = ImmediateTextNode() self.codeTextNode.displaysAsynchronously = false @@ -1550,14 +1560,19 @@ private class QrContentNode: ASDisplayNode, ContentNode { self.addSubnode(codeAnimatedIconNode) } - let codeLink: String + var codeLink: String if let addressName = peer.addressName, !addressName.isEmpty { codeLink = "https://t.me/\(peer.addressName ?? "")" } else if let peer = peer as? TelegramUser { codeLink = "https://t.me/+\(peer.phone ?? "")" + } else if let _ = peer as? TelegramChannel { + codeLink = "https://t.me/c/\(peer.id.id._internalGetInt64Value())" } else { codeLink = "" } + if let threadId = threadId { + codeLink += "?topic=\(threadId)" + } let codeReadyPromise = ValuePromise() self.codeImageNode.setSignal(qrCode(string: codeLink, color: .black, backgroundColor: nil, icon: .cutout, ecl: "Q") |> beforeNext { [weak self] size, _ in @@ -1591,7 +1606,7 @@ private class QrContentNode: ASDisplayNode, ContentNode { let size = CGSize(width: 390.0, height: 844.0) let scale: CGFloat = 3.0 - let copyNode = QrContentNode(context: self.context, peer: self.peer, isStatic: true) + let copyNode = QrContentNode(context: self.context, peer: self.peer, threadId: self.threadId, isStatic: true) func prepare(view: UIView, scale: CGFloat) { view.contentScaleFactor = scale diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 3ad78aa314..b4f9f4c6ea 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -230,6 +230,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in + }, deletePeerThread: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in }, toggleArchivedFolderHiddenByDefault: { diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 663e033788..2f57f5bce9 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1408,7 +1408,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } else if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) { placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder - } else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation { + } else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost { if replyThreadMessage.isChannelPost { placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment } else { diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift index fb9e5e0534..b59833c101 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift @@ -19,6 +19,8 @@ import ChatPresentationInterfaceState final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { private let context: AccountContext private let peerId: PeerId + private let chatLocation: ChatLocation + private let chatLocationContextHolder: Atomic private let chatControllerInteraction: ChatControllerInteraction weak var parentController: ViewController? @@ -61,16 +63,18 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { return 0.0 } - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, tagMask: MessageTags) { + init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tagMask: MessageTags) { self.context = context self.peerId = peerId + self.chatLocation = chatLocation + self.chatLocationContextHolder = chatLocationContextHolder + self.chatControllerInteraction = chatControllerInteraction self.selectedMessages = chatControllerInteraction.selectionState.flatMap { $0.selectedIds } self.selectedMessagesPromise.set(.single(self.selectedMessages)) - let chatLocationContextHolder = Atomic(value: nil) - self.listNode = ChatHistoryListNode(context: context, updatedPresentationData: updatedPresentationData ?? (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: .peer(id: peerId), chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, subject: nil, controllerInteraction: chatControllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false, displayHeaders: .allButLast, hintLinks: tagMask == .webPage, isGlobalSearch: false)) + self.listNode = ChatHistoryListNode(context: context, updatedPresentationData: updatedPresentationData ?? (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, subject: nil, controllerInteraction: chatControllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false, displayHeaders: .allButLast, hintLinks: tagMask == .webPage, isGlobalSearch: false)) self.listNode.clipsToBounds = true self.listNode.defaultToSynchronousTransactionWhileScrolling = true self.listNode.scroller.bounces = false diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index 2d175e3795..56a39b7727 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -1594,6 +1594,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro private let context: AccountContext private let peerId: PeerId + private let chatLocation: ChatLocation + private let chatLocationContextHolder: Atomic private let chatControllerInteraction: ChatControllerInteraction private(set) var contentType: ContentType private var contentTypePromise: ValuePromise @@ -1657,9 +1659,11 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, contentType: ContentType, captureProtected: Bool) { + init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, contentType: ContentType, captureProtected: Bool) { self.context = context self.peerId = peerId + self.chatLocation = chatLocation + self.chatLocationContextHolder = chatLocationContextHolder self.chatControllerInteraction = chatControllerInteraction self.contentType = contentType self.contentTypePromise = ValuePromise(contentType) @@ -1719,12 +1723,21 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro directMediaImageCache: self.directMediaImageCache, captureProtected: captureProtected ) + + var threadId: Int64? + if case let .replyThread(message) = chatLocation { + threadId = Int64(message.messageId.id) + } - self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, tag: tagMaskForType(self.contentType)) - switch contentType { - case .photoOrVideo, .photo, .video: - self.calendarSource = self.context.engine.messages.sparseMessageCalendar(peerId: self.peerId, tag: tagMaskForType(self.contentType)) - default: + self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, threadId: threadId, tag: tagMaskForType(self.contentType)) + if threadId == nil { + switch contentType { + case .photoOrVideo, .photo, .video: + self.calendarSource = self.context.engine.messages.sparseMessageCalendar(peerId: self.peerId, tag: tagMaskForType(self.contentType)) + default: + self.calendarSource = nil + } + } else { self.calendarSource = nil } @@ -2172,8 +2185,13 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro self.contentTypePromise.set(contentType) self.itemGrid.hideScrollingArea() + + var threadId: Int64? + if case let .replyThread(message) = chatLocation { + threadId = Int64(message.messageId.id) + } - self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, tag: tagMaskForType(self.contentType)) + self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, threadId: threadId, tag: tagMaskForType(self.contentType)) self.isRequestingView = false self.requestHistoryAroundVisiblePosition(synchronous: true, reloadAtTop: true) } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index d1b218dcce..46f26eb955 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -187,6 +187,7 @@ final class PeerInfoScreenData { let invitations: PeerExportedInvitationsState? let requests: PeerInvitationImportersState? let requestsContext: PeerInvitationImportersContext? + let threadInfo: EngineMessageHistoryThread.Info? init( peer: Peer?, @@ -204,7 +205,8 @@ final class PeerInfoScreenData { globalSettings: TelegramGlobalSettings?, invitations: PeerExportedInvitationsState?, requests: PeerInvitationImportersState?, - requestsContext: PeerInvitationImportersContext? + requestsContext: PeerInvitationImportersContext?, + threadInfo: EngineMessageHistoryThread.Info? ) { self.peer = peer self.chatPeer = chatPeer @@ -222,6 +224,7 @@ final class PeerInfoScreenData { self.invitations = invitations self.requests = requests self.requestsContext = requestsContext + self.threadInfo = threadInfo } } @@ -240,7 +243,7 @@ private enum PeerInfoScreenInputData: Equatable { case group(groupId: PeerId) } -private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId) -> Signal<[PeerInfoPaneKey]?, NoError> { +private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic) -> Signal<[PeerInfoPaneKey]?, NoError> { let tags: [(MessageTags, PeerInfoPaneKey)] = [ (.photoOrVideo, .media), (.file, .files), @@ -257,7 +260,8 @@ private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId let loadedOnce = Atomic(value: false) return combineLatest(queue: .mainQueue(), tags.map { tagAndKey -> Signal<(PeerInfoPaneKey, PaneState), NoError> in let (tag, key) = tagAndKey - return context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId), index: .upperBound, anchorIndex: .upperBound, count: 20, clipHoles: false, fixedCombinedReadStates: nil, tagMask: tag) + let location = context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder) + return context.account.viewTracker.aroundMessageHistoryViewForLocation(location, index: .upperBound, anchorIndex: .upperBound, count: 20, clipHoles: false, fixedCombinedReadStates: nil, tagMask: tag) |> map { (view, _, _) -> (PeerInfoPaneKey, PaneState) in if view.entries.isEmpty { if view.isLoading { @@ -353,7 +357,7 @@ private func peerInfoScreenInputData(context: AccountContext, peerId: EnginePeer |> distinctUntilChanged } -func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId) -> Signal { +func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic) -> Signal { return peerInfoScreenInputData(context: context, peerId: peerId, isSettings: false) |> mapToSignal { inputData -> Signal in switch inputData { @@ -361,7 +365,7 @@ func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId) -> Signa return .complete() case .user, .channel, .group: return combineLatest( - context.peerChannelMemberCategoriesContextsManager.profileData(postbox: context.account.postbox, network: context.account.network, peerId: peerId, customData: peerInfoAvailableMediaPanes(context: context, peerId: peerId) |> ignoreValues), + context.peerChannelMemberCategoriesContextsManager.profileData(postbox: context.account.postbox, network: context.account.network, peerId: peerId, customData: peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder) |> ignoreValues), context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: context.account.postbox, network: context.account.network, peerId: peerId, fetch: peerInfoProfilePhotos(context: context, peerId: peerId)) |> ignoreValues ) |> ignoreValues @@ -472,12 +476,13 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, globalSettings: globalSettings, invitations: nil, requests: nil, - requestsContext: nil + requestsContext: nil, + threadInfo: nil ) } } -func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, isSettings: Bool, hintGroupInCommon: PeerId?, existingRequestsContext: PeerInvitationImportersContext?) -> Signal { +func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, isSettings: Bool, hintGroupInCommon: PeerId?, existingRequestsContext: PeerInvitationImportersContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic) -> Signal { return peerInfoScreenInputData(context: context, peerId: peerId, isSettings: isSettings) |> mapToSignal { inputData -> Signal in switch inputData { @@ -498,7 +503,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen globalSettings: nil, invitations: nil, requests: nil, - requestsContext: nil + requestsContext: nil, + threadInfo: nil )) case let .user(userPeerId, secretChatId, kind): let groupsInCommon: GroupsInCommonContext? @@ -599,7 +605,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), - peerInfoAvailableMediaPanes(context: context, peerId: peerId), + peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder), context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()), secretChatKeyFingerprint, status @@ -628,7 +634,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen globalSettings: nil, invitations: nil, requests: nil, - requestsContext: nil + requestsContext: nil, + threadInfo: nil ) } case .channel: @@ -653,7 +660,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), - peerInfoAvailableMediaPanes(context: context, peerId: peerId), + peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder), context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()), status, invitationsContextPromise.get(), @@ -703,7 +710,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen globalSettings: nil, invitations: invitations, requests: requests, - requestsContext: currentRequestsContext + requestsContext: currentRequestsContext, + threadInfo: nil ) } case let .group(groupId): @@ -785,17 +793,21 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } |> distinctUntilChanged - let membersContext = PeerInfoMembersContext(context: context, peerId: groupId) - - let membersData: Signal = combineLatest(membersContext.state, context.account.viewTracker.peerView(groupId, updateData: false)) - |> map { state, view -> PeerInfoMembersData? in - if state.members.count > 5 { - return .longList(membersContext) - } else { - return .shortList(membersContext: membersContext, members: state.members) + let membersData: Signal + if case .peer = chatLocation { + let membersContext = PeerInfoMembersContext(context: context, peerId: groupId) + membersData = combineLatest(membersContext.state, context.account.viewTracker.peerView(groupId, updateData: false)) + |> map { state, view -> PeerInfoMembersData? in + if state.members.count > 5 { + return .longList(membersContext) + } else { + return .shortList(membersContext: membersContext, members: state.members) + } } + |> distinctUntilChanged + } else { + membersData = .single(nil) } - |> distinctUntilChanged let invitationsContextPromise = Promise(nil) let invitationsStatePromise = Promise(nil) @@ -803,27 +815,40 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let requestsContextPromise = Promise(nil) let requestsStatePromise = Promise(nil) + let threadInfo: Signal + if case let .replyThread(message) = chatLocation { + let threadId = Int64(message.messageId.id) + let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: threadId) + threadInfo = context.account.postbox.combinedView(keys: [viewKey]) + |> map { views -> EngineMessageHistoryThread.Info? in + guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else { + return nil + } + return view.info?.get(MessageHistoryThreadData.self)?.info + } + } else { + threadInfo = .single(nil) + } + return combineLatest(queue: .mainQueue(), context.account.viewTracker.peerView(groupId, updateData: true), - peerInfoAvailableMediaPanes(context: context, peerId: groupId), + peerInfoAvailableMediaPanes(context: context, peerId: groupId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder), context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()), status, membersData, invitationsContextPromise.get(), invitationsStatePromise.get(), requestsContextPromise.get(), - requestsStatePromise.get() + requestsStatePromise.get(), + threadInfo ) - |> map { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, threadInfo -> PeerInfoScreenData in var discussionPeer: Peer? if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] { discussionPeer = peer } var availablePanes = availablePanes - if let channel = peerView.peers[peerView.peerId] as? TelegramChannel, channel.flags.contains(.isForum) { - availablePanes?.removeAll() - } if let membersData = membersData, case .longList = membersData { if availablePanes != nil { availablePanes?.insert(.members, at: 0) @@ -874,7 +899,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen globalSettings: nil, invitations: invitations, requests: requests, - requestsContext: currentRequestsContext + requestsContext: currentRequestsContext, + threadInfo: threadInfo ) } } @@ -1013,7 +1039,7 @@ func peerInfoHeaderButtonIsHiddenWhileExpanded(buttonKey: PeerInfoHeaderButtonKe return hiddenWhileExpanded } -func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool, isExpanded: Bool, videoCallsEnabled: Bool, isSecretChat: Bool, isContact: Bool) -> [PeerInfoHeaderButtonKey] { +func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool, isExpanded: Bool, videoCallsEnabled: Bool, isSecretChat: Bool, isContact: Bool, threadInfo: EngineMessageHistoryThread.Info?) -> [PeerInfoHeaderButtonKey] { var result: [PeerInfoHeaderButtonKey] = [] if let user = peer as? TelegramUser { if !isOpenedFromChat { @@ -1050,69 +1076,74 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro result.append(.more) } } else if let channel = peer as? TelegramChannel { - let hasVoiceChat = channel.flags.contains(.hasVoiceChat) - let canStartVoiceChat = !hasVoiceChat && (channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls)) - let canManage = channel.flags.contains(.isCreator) || channel.adminRights != nil + if let _ = threadInfo { + result.append(.mute) + result.append(.search) + } else { + let hasVoiceChat = channel.flags.contains(.hasVoiceChat) + let canStartVoiceChat = !hasVoiceChat && (channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls)) + let canManage = channel.flags.contains(.isCreator) || channel.adminRights != nil - let hasDiscussion: Bool - switch channel.info { + let hasDiscussion: Bool + switch channel.info { case let .broadcast(info): hasDiscussion = info.flags.contains(.hasDiscussionGroup) case .group: hasDiscussion = false - } - - let canLeave: Bool - switch channel.participationStatus { - case .member: - canLeave = true - default: - canLeave = false - } - - let canViewStats: Bool - if let cachedChannelData = cachedData as? CachedChannelData { - canViewStats = cachedChannelData.flags.contains(.canViewStats) - } else { - canViewStats = false - } - - if hasVoiceChat || canStartVoiceChat { - result.append(.voiceChat) - } - result.append(.mute) - if hasDiscussion { - result.append(.discussion) - } - result.append(.search) - if canLeave { - result.append(.leave) - } - - var canReport = true - if channel.isVerified || channel.adminRights != nil || channel.flags.contains(.isCreator) { - canReport = false - } - - var hasMore = false - if canReport || canViewStats { - hasMore = true - result.append(.more) - } - - if hasDiscussion && isExpanded && result.count >= 5 { - result.removeAll(where: { $0 == .search }) - if !hasMore { + } + + let canLeave: Bool + switch channel.participationStatus { + case .member: + canLeave = true + default: + canLeave = false + } + + let canViewStats: Bool + if let cachedChannelData = cachedData as? CachedChannelData { + canViewStats = cachedChannelData.flags.contains(.canViewStats) + } else { + canViewStats = false + } + + if hasVoiceChat || canStartVoiceChat { + result.append(.voiceChat) + } + result.append(.mute) + if hasDiscussion { + result.append(.discussion) + } + result.append(.search) + if canLeave { + result.append(.leave) + } + + var canReport = true + if channel.isVerified || channel.adminRights != nil || channel.flags.contains(.isCreator) { + canReport = false + } + + var hasMore = false + if canReport || canViewStats { hasMore = true result.append(.more) } - } - - if canLeave && isExpanded && (canManage || result.count >= 5) { - result.removeAll(where: { $0 == .leave }) - if !hasMore { - hasMore = true - result.append(.more) + + if hasDiscussion && isExpanded && result.count >= 5 { + result.removeAll(where: { $0 == .search }) + if !hasMore { + hasMore = true + result.append(.more) + } + } + + if canLeave && isExpanded && (canManage || result.count >= 5) { + result.removeAll(where: { $0 == .leave }) + if !hasMore { + hasMore = true + result.append(.more) + } } } } else if let group = peer as? TelegramGroup { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 24eae0d90b..0065dec29e 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -308,6 +308,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { let avatarNode: AvatarNode fileprivate var videoNode: UniversalVideoNode? + fileprivate var iconView: ComponentView? private var videoContent: NativeVideoContent? private var videoStartTimestamp: Double? @@ -380,7 +381,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { } var removedPhotoResourceIds = Set() - func update(peer: Peer?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) { + func update(peer: Peer?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) { if let peer = peer { let previousItem = self.item var item = item @@ -410,6 +411,43 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: avatarSize, height: avatarSize), storeUnrounded: true) + if let threadInfo = threadInfo { + self.avatarNode.isHidden = true + + let iconView: ComponentView + if let current = self.iconView { + iconView = current + } else { + iconView = ComponentView() + self.iconView = iconView + } + let content: EmojiStatusComponent.Content + if let iconFileId = threadInfo.icon { + content = .animation(content: .customEmoji(fileId: iconFileId), size: CGSize(width: avatarSize, height: avatarSize), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: nil, loopMode: .forever) + } else { + content = .topic(title: String(threadInfo.title.prefix(1)), colorIndex: Int(threadInfo.iconColor), size: CGSize(width: avatarSize, height: avatarSize)) + } + let _ = iconView.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + content: content, + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: CGSize(width: avatarSize, height: avatarSize) + ) + if let iconComponentView = iconView.view { + if iconComponentView.superview == nil { + self.avatarNode.view.superview?.addSubview(iconComponentView) + } + iconComponentView.frame = CGRect(origin: CGPoint(), size: CGSize(width: avatarSize, height: avatarSize)) + } + } + let avatarCornerRadius: CGFloat if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { avatarCornerRadius = floor(avatarSize * 0.25) @@ -825,7 +863,7 @@ final class PeerInfoAvatarListNode: ASDisplayNode { let isReady = Promise() - var arguments: (Peer?, PresentationTheme, CGFloat, Bool)? + var arguments: (Peer?, EngineMessageHistoryThread.Info?, PresentationTheme, CGFloat, Bool)? var item: PeerInfoAvatarListItem? var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)? @@ -884,14 +922,14 @@ final class PeerInfoAvatarListNode: ASDisplayNode { if let strongSelf = self { strongSelf.item = items.first strongSelf.itemsUpdated?(items) - if let (peer, theme, avatarSize, isExpanded) = strongSelf.arguments { - strongSelf.avatarContainerNode.update(peer: peer, item: strongSelf.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: strongSelf.isSettings) + if let (peer, threadInfo, theme, avatarSize, isExpanded) = strongSelf.arguments { + strongSelf.avatarContainerNode.update(peer: peer, threadInfo: threadInfo, item: strongSelf.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: strongSelf.isSettings) } } } self.pinchSourceNode.activate = { [weak self] sourceNode in - guard let strongSelf = self, let (_, _, _, isExpanded) = strongSelf.arguments, isExpanded else { + guard let strongSelf = self, let (_, _, _, _, isExpanded) = strongSelf.arguments, isExpanded else { return } let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { @@ -910,11 +948,11 @@ final class PeerInfoAvatarListNode: ASDisplayNode { } } - func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { - self.arguments = (peer, theme, avatarSize, isExpanded) + func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { + self.arguments = (peer, threadInfo, theme, avatarSize, isExpanded) self.pinchSourceNode.update(size: size, transition: transition) self.pinchSourceNode.frame = CGRect(origin: CGPoint(), size: size) - self.avatarContainerNode.update(peer: peer, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings) + self.avatarContainerNode.update(peer: peer, threadInfo: threadInfo, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -1996,6 +2034,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { private let isOpenedFromChat: Bool private let isSettings: Bool private let videoCallsEnabled: Bool + private let forumTopicThreadId: Int64? private(set) var isAvatarExpanded: Bool var skipCollapseCompletion = false @@ -2056,12 +2095,13 @@ final class PeerInfoHeaderNode: ASDisplayNode { var emojiStatusPackDisposable = MetaDisposable() var emojiStatusFileAndPackTitle = Promise<(TelegramMediaFile, LoadedStickerPack)?>() - init(context: AccountContext, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, isMediaOnly: Bool, isSettings: Bool) { + init(context: AccountContext, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, isMediaOnly: Bool, isSettings: Bool, forumTopicThreadId: Int64?) { self.context = context self.isAvatarExpanded = avatarInitiallyExpanded self.isOpenedFromChat = isOpenedFromChat self.isSettings = isSettings self.videoCallsEnabled = true + self.forumTopicThreadId = forumTopicThreadId self.avatarListNode = PeerInfoAvatarListNode(context: context, readyWhenGalleryLoads: avatarInitiallyExpanded, isSettings: isSettings) @@ -2304,7 +2344,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { private var currentCredibilityIcon: CredibilityIcon? private var currentPanelStatusData: PeerInfoStatusData? - func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, metrics: LayoutMetrics, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { + func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, threadInfo: EngineMessageHistoryThread.Info?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, metrics: LayoutMetrics, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { self.state = state self.peer = peer self.avatarListNode.listContainerNode.peer = peer @@ -2527,7 +2567,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let expandedAvatarListHeight = min(width, containerHeight - expandedAvatarControlsHeight) let expandedAvatarListSize = CGSize(width: width, height: expandedAvatarListHeight) - let buttonKeys: [PeerInfoHeaderButtonKey] = self.isSettings ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: true, videoCallsEnabled: width > 320.0 && self.videoCallsEnabled, isSecretChat: isSecretChat, isContact: isContact) + let buttonKeys: [PeerInfoHeaderButtonKey] = self.isSettings ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: true, videoCallsEnabled: width > 320.0 && self.videoCallsEnabled, isSecretChat: isSecretChat, isContact: isContact, threadInfo: threadInfo) var isPremium = false var isVerified = false @@ -2551,6 +2591,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { title = presentationData.strings.Conversation_SavedMessages } else if peer.id == self.context.account.peerId && !self.isSettings { title = presentationData.strings.DialogList_Replies + } else if let threadInfo = threadInfo { + title = threadInfo.title } else { title = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) } @@ -2577,6 +2619,34 @@ final class PeerInfoHeaderNode: ASDisplayNode { smallSubtitleString = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7)) subtitleString = NSAttributedString(string: subtitle, font: Font.regular(17.0), textColor: presentationData.theme.list.itemSecondaryTextColor) usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + } else if let _ = threadInfo { + let subtitleColor: UIColor = presentationData.theme.list.itemSecondaryTextColor + + //TODO:localize + var statusText = "in " + if let addressName = peer.addressName { + statusText += "@\(addressName)" + } else { + statusText += peer.debugDisplayTitle + } + + smallSubtitleString = NSAttributedString(string: statusText, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7)) + subtitleString = NSAttributedString(string: statusText, font: Font.regular(17.0), textColor: subtitleColor) + usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + + let (maybePanelStatusData, maybeNextPanelStatusData, _) = panelStatusData + if let panelStatusData = maybePanelStatusData { + let subtitleColor: UIColor + if panelStatusData.isActivity { + subtitleColor = presentationData.theme.list.itemAccentColor + } else { + subtitleColor = presentationData.theme.list.itemSecondaryTextColor + } + panelSubtitleString = NSAttributedString(string: panelStatusData.text, font: Font.regular(17.0), textColor: subtitleColor) + } + if let nextPanelStatusData = maybeNextPanelStatusData { + nextPanelSubtitleString = NSAttributedString(string: nextPanelStatusData.text, font: Font.regular(17.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + } } else if let statusData = statusData { let subtitleColor: UIColor if statusData.isActivity { @@ -2839,7 +2909,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { }) } - self.avatarListNode.update(size: CGSize(), avatarSize: avatarSize, isExpanded: self.isAvatarExpanded, peer: peer, theme: presentationData.theme, transition: transition) + self.avatarListNode.update(size: CGSize(), avatarSize: avatarSize, isExpanded: self.isAvatarExpanded, peer: peer, threadInfo: threadInfo, theme: presentationData.theme, transition: transition) self.editingContentNode.avatarNode.update(peer: peer, item: self.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing) self.avatarOverlayNode.update(peer: peer, item: self.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing) if additive { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift index 3ad85bb14e..ed2435ca9a 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift @@ -386,6 +386,8 @@ private final class PeerInfoPendingPane { openAddMemberAction: @escaping () -> Void, requestPerformPeerMemberAction: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, peerId: PeerId, + chatLocation: ChatLocation, + chatLocationContextHolder: Atomic, key: PeerInfoPaneKey, hasBecomeReady: @escaping (PeerInfoPaneKey) -> Void, parentController: ViewController?, @@ -396,7 +398,7 @@ private final class PeerInfoPendingPane { let paneNode: PeerInfoPaneNode switch key { case .media: - let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .photoOrVideo, captureProtected: captureProtected) + let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .photoOrVideo, captureProtected: captureProtected) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() @@ -405,21 +407,19 @@ private final class PeerInfoPendingPane { paneDidScroll() } case .files: - let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .files, captureProtected: captureProtected) + let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .files, captureProtected: captureProtected) paneNode = visualPaneNode //paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file) case .links: - paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .webPage) + paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: .webPage) case .voice: - let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .voiceAndVideoMessages, captureProtected: captureProtected) + let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .voiceAndVideoMessages, captureProtected: captureProtected) paneNode = visualPaneNode - //paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .voiceOrInstantVideo) case .music: - let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .music, captureProtected: captureProtected) + let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .music, captureProtected: captureProtected) paneNode = visualPaneNode - //paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .music) case .gifs: - let visualPaneNode = PeerInfoGifPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .gifs) + let visualPaneNode = PeerInfoGifPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .gifs) paneNode = visualPaneNode case .groupsInCommon: paneNode = PeerInfoGroupsInCommonPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction, groupsInCommonContext: data.groupsInCommon!) @@ -452,6 +452,8 @@ private final class PeerInfoPendingPane { final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private let context: AccountContext private let peerId: PeerId + private let chatLocation: ChatLocation + private let chatLocationContextHolder: Atomic private let isMediaOnly: Bool weak var parentController: ViewController? @@ -509,10 +511,12 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat private var currentAvailablePanes: [PeerInfoPaneKey]? private let updatedPresentationData: (initial: PresentationData, signal: Signal)? - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, isMediaOnly: Bool) { + init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, isMediaOnly: Bool) { self.context = context self.updatedPresentationData = updatedPresentationData self.peerId = peerId + self.chatLocation = chatLocation + self.chatLocationContextHolder = chatLocationContextHolder self.isMediaOnly = isMediaOnly self.additionalBackgroundNode = ASDisplayNode() @@ -807,6 +811,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat self?.requestPerformPeerMemberAction?(member, action) }, peerId: self.peerId, + chatLocation: self.chatLocation, + chatLocationContextHolder: self.chatLocationContextHolder, key: key, hasBecomeReady: { [weak self] key in let apply: () -> Void = { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index d39555f3f2..f7185661c3 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -77,6 +77,7 @@ import AvatarNode import ComponentFlow import EmojiStatusComponent import ChatTitleView +import ForumCreateTopicScreen protocol PeerInfoScreenItem: AnyObject { var id: AnyHashable { get } @@ -423,7 +424,7 @@ private enum PeerInfoMemberAction { private enum PeerInfoContextSubject { case bio case phone(String) - case link + case link(customLink: String?) } private enum PeerInfoSettingsSection { @@ -915,7 +916,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat return result } -private func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message]) -> [(AnyHashable, [PeerInfoScreenItem])] { +private func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], chatLocation: ChatLocation) -> [(AnyHashable, [PeerInfoScreenItem])] { guard let data = data else { return [] } @@ -973,7 +974,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese action: { _ in interaction.openUsername(username) }, longTapAction: { sourceNode in - interaction.openPeerInfoContextMenu(.link, sourceNode) + interaction.openPeerInfoContextMenu(.link(customLink: nil), sourceNode) }, linkItemAction: { type, item in if case .tap = type { if case let .mention(username) = item { @@ -1077,42 +1078,32 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese let ItemMembers = 6 let ItemMemberRequests = 7 - if let location = (data.cachedData as? CachedChannelData)?.peerGeoLocation { - items[.groupLocation]!.append(PeerInfoScreenHeaderItem(id: ItemLocationHeader, text: presentationData.strings.GroupInfo_Location.uppercased())) - - let imageSignal = chatMapSnapshotImage(engine: context.engine, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90)) - items[.groupLocation]!.append(PeerInfoScreenAddressItem( - id: ItemLocation, - label: "", - text: location.address.replacingOccurrences(of: ", ", with: "\n"), - imageSignal: imageSignal, - action: { - interaction.openLocation() - } - )) - } - - if let username = channel.username { - var additionalUsernames: String? - let mainUsername = channel.usernames.first?.username ?? username - - let usernames = channel.usernames.filter { $0.flags.contains(.isActive) && $0.username != mainUsername } - if !usernames.isEmpty { - additionalUsernames = presentationData.strings.Profile_AdditionalUsernames(String(usernames.map { "@\($0.username)" }.joined(separator: ", "))).string + if let _ = data.threadInfo { + let mainUsername: String + if let addressName = channel.addressName { + mainUsername = addressName + } else { + mainUsername = "c/\(channel.id.id._internalGetInt64Value())" } + var threadId: Int64 = 0 + if case let .replyThread(message) = chatLocation { + threadId = Int64(message.messageId.id) + } + + let linkText = "https://t.me/\(mainUsername)?topic=\(threadId)" + items[.peerInfo]!.append( PeerInfoScreenLabeledValueItem( id: ItemUsername, label: presentationData.strings.Channel_LinkItem, - text: "https://t.me/\(mainUsername)", - additionalText: additionalUsernames, + text: linkText, textColor: .accent, icon: .qrCode, action: { _ in - interaction.openUsername(username) + interaction.openUsername(linkText) }, longTapAction: { sourceNode in - interaction.openPeerInfoContextMenu(.link, sourceNode) + interaction.openPeerInfoContextMenu(.link(customLink: linkText), sourceNode) }, linkItemAction: { type, item in if case .tap = type { if case let .mention(username) = item { @@ -1126,58 +1117,109 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } ) ) - } - if let cachedData = data.cachedData as? CachedChannelData { - let aboutText: String? - if channel.isFake { - if case .broadcast = channel.info { - aboutText = presentationData.strings.ChannelInfo_FakeChannelWarning - } else { - aboutText = presentationData.strings.GroupInfo_FakeGroupWarning - } - } else if channel.isScam { - if case .broadcast = channel.info { - aboutText = presentationData.strings.ChannelInfo_ScamChannelWarning - } else { - aboutText = presentationData.strings.GroupInfo_ScamGroupWarning - } - } else if let about = cachedData.about, !about.isEmpty { - aboutText = about - } else { - aboutText = nil + } else { + if let location = (data.cachedData as? CachedChannelData)?.peerGeoLocation { + items[.groupLocation]!.append(PeerInfoScreenHeaderItem(id: ItemLocationHeader, text: presentationData.strings.GroupInfo_Location.uppercased())) + + let imageSignal = chatMapSnapshotImage(engine: context.engine, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90)) + items[.groupLocation]!.append(PeerInfoScreenAddressItem( + id: ItemLocation, + label: "", + text: location.address.replacingOccurrences(of: ", ", with: "\n"), + imageSignal: imageSignal, + action: { + interaction.openLocation() + } + )) } - if let aboutText = aboutText { - var enabledEntities = enabledPublicBioEntities - if case .group = channel.info { - enabledEntities = enabledPrivateBioEntities + if let username = channel.username { + var additionalUsernames: String? + let mainUsername = channel.usernames.first?.username ?? username + + let usernames = channel.usernames.filter { $0.flags.contains(.isActive) && $0.username != mainUsername } + if !usernames.isEmpty { + additionalUsernames = presentationData.strings.Profile_AdditionalUsernames(String(usernames.map { "@\($0.username)" }.joined(separator: ", "))).string } - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { - interaction.requestLayout(true) - })) + + items[.peerInfo]!.append( + PeerInfoScreenLabeledValueItem( + id: ItemUsername, + label: presentationData.strings.Channel_LinkItem, + text: "https://t.me/\(mainUsername)", + additionalText: additionalUsernames, + textColor: .accent, + icon: .qrCode, + action: { _ in + interaction.openUsername(username) + }, longTapAction: { sourceNode in + interaction.openPeerInfoContextMenu(.link(customLink: nil), sourceNode) + }, linkItemAction: { type, item in + if case .tap = type { + if case let .mention(username) = item { + interaction.openUsername(String(username.suffix(from: username.index(username.startIndex, offsetBy: 1)))) + } + } + }, iconAction: { + interaction.openQrCode() + }, requestLayout: { + interaction.requestLayout(false) + } + ) + ) } - - if case .broadcast = channel.info { - var canEditMembers = false - if channel.hasPermission(.banMembers) { - canEditMembers = true + if let cachedData = data.cachedData as? CachedChannelData { + let aboutText: String? + if channel.isFake { + if case .broadcast = channel.info { + aboutText = presentationData.strings.ChannelInfo_FakeChannelWarning + } else { + aboutText = presentationData.strings.GroupInfo_FakeGroupWarning + } + } else if channel.isScam { + if case .broadcast = channel.info { + aboutText = presentationData.strings.ChannelInfo_ScamChannelWarning + } else { + aboutText = presentationData.strings.GroupInfo_ScamGroupWarning + } + } else if let about = cachedData.about, !about.isEmpty { + aboutText = about + } else { + aboutText = nil } - if canEditMembers { - if channel.adminRights != nil || channel.flags.contains(.isCreator) { - let adminCount = cachedData.participantsSummary.adminCount ?? 0 - let memberCount = cachedData.participantsSummary.memberCount ?? 0 - - items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: .text("\(adminCount == 0 ? "" : "\(presentationStringsFormattedNumber(adminCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Administrators, icon: UIImage(bundleImageName: "Chat/Info/GroupAdminsIcon"), action: { - interaction.openParticipantsSection(.admins) - })) - items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemMembers, label: .text("\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.Channel_Info_Subscribers, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: { - interaction.openParticipantsSection(.members) - })) - - if let count = data.requests?.count, count > 0 { - items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemMemberRequests, label: .badge(presentationStringsFormattedNumber(count, presentationData.dateTimeFormat.groupingSeparator), presentationData.theme.list.itemAccentColor), text: presentationData.strings.GroupInfo_MemberRequests, icon: UIImage(bundleImageName: "Chat/Info/GroupRequestsIcon"), action: { - interaction.openParticipantsSection(.memberRequests) + + if let aboutText = aboutText { + var enabledEntities = enabledPublicBioEntities + if case .group = channel.info { + enabledEntities = enabledPrivateBioEntities + } + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { + interaction.requestLayout(true) + })) + } + + if case .broadcast = channel.info { + var canEditMembers = false + if channel.hasPermission(.banMembers) { + canEditMembers = true + } + if canEditMembers { + if channel.adminRights != nil || channel.flags.contains(.isCreator) { + let adminCount = cachedData.participantsSummary.adminCount ?? 0 + let memberCount = cachedData.participantsSummary.memberCount ?? 0 + + items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: .text("\(adminCount == 0 ? "" : "\(presentationStringsFormattedNumber(adminCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Administrators, icon: UIImage(bundleImageName: "Chat/Info/GroupAdminsIcon"), action: { + interaction.openParticipantsSection(.admins) })) + items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemMembers, label: .text("\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.Channel_Info_Subscribers, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: { + interaction.openParticipantsSection(.members) + })) + + if let count = data.requests?.count, count > 0 { + items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemMemberRequests, label: .badge(presentationStringsFormattedNumber(count, presentationData.dateTimeFormat.groupingSeparator), presentationData.theme.list.itemAccentColor), text: presentationData.strings.GroupInfo_MemberRequests, icon: UIImage(bundleImageName: "Chat/Info/GroupRequestsIcon"), action: { + interaction.openParticipantsSection(.memberRequests) + })) + } } } } @@ -1740,6 +1782,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate private let isOpenedFromChat: Bool private let videoCallsEnabled: Bool private let callMessages: [Message] + private let chatLocation: ChatLocation + private let chatLocationContextHolder: Atomic let isSettings: Bool private let isMediaOnly: Bool @@ -1834,7 +1878,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } private var didSetReady = false - init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?) { + init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic) { self.controller = controller self.context = context self.peerId = peerId @@ -1845,14 +1889,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate self.reactionSourceMessageId = reactionSourceMessageId self.callMessages = callMessages self.isSettings = isSettings + self.chatLocation = chatLocation + self.chatLocationContextHolder = chatLocationContextHolder self.isMediaOnly = context.account.peerId == peerId && !isSettings self.scrollNode = ASScrollNode() self.scrollNode.view.delaysContentTouches = false self.scrollNode.canCancelAllTouchesInViews = true - self.headerNode = PeerInfoHeaderNode(context: context, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, isMediaOnly: self.isMediaOnly, isSettings: isSettings) - self.paneContainerNode = PeerInfoPaneContainerNode(context: context, updatedPresentationData: controller.updatedPresentationData, peerId: peerId, isMediaOnly: self.isMediaOnly) + var forumTopicThreadId: Int64? + if case let .replyThread(message) = chatLocation { + forumTopicThreadId = Int64(message.messageId.id) + } + self.headerNode = PeerInfoHeaderNode(context: context, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, isMediaOnly: self.isMediaOnly, isSettings: isSettings, forumTopicThreadId: forumTopicThreadId) + self.paneContainerNode = PeerInfoPaneContainerNode(context: context, updatedPresentationData: controller.updatedPresentationData, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, isMediaOnly: self.isMediaOnly) super.init() @@ -2765,36 +2815,52 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } switch key { case .edit: - (strongSelf.controller?.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.3, curve: .linear)) - strongSelf.state = strongSelf.state.withIsEditing(true) - var updateOnCompletion = false - if strongSelf.headerNode.isAvatarExpanded { - updateOnCompletion = true - strongSelf.headerNode.skipCollapseCompletion = true - strongSelf.headerNode.avatarListNode.avatarContainerNode.canAttachVideo = false - strongSelf.headerNode.editingContentNode.avatarNode.canAttachVideo = false - strongSelf.headerNode.avatarListNode.listContainerNode.isCollapsing = true - strongSelf.headerNode.updateIsAvatarExpanded(false, transition: .immediate) - strongSelf.updateNavigationExpansionPresentation(isExpanded: false, animated: true) - } - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.scrollNode.view.setContentOffset(CGPoint(), animated: false) - strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) - } - UIView.transition(with: strongSelf.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { - }, completion: { _ in - if updateOnCompletion { - strongSelf.headerNode.skipCollapseCompletion = false - strongSelf.headerNode.avatarListNode.listContainerNode.isCollapsing = false - strongSelf.headerNode.avatarListNode.avatarContainerNode.canAttachVideo = true - strongSelf.headerNode.editingContentNode.avatarNode.canAttachVideo = true - strongSelf.headerNode.editingContentNode.avatarNode.reset() - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + if case let .replyThread(message) = strongSelf.chatLocation { + let threadId = Int64(message.messageId.id) + if let threadInfo = strongSelf.data?.threadInfo { + let controller = ForumCreateTopicScreen(context: strongSelf.context, peerId: strongSelf.peerId, mode: .edit(topic: threadInfo)) + controller.navigationPresentation = .modal + let context = strongSelf.context + controller.completion = { [weak controller] title, fileId in + let _ = (context.engine.peers.editForumChannelTopic(id: peerId, threadId: threadId, title: title, iconFileId: fileId) + |> deliverOnMainQueue).start(completed: { + controller?.dismiss() + }) } + strongSelf.controller?.push(controller) } - }) - strongSelf.controller?.navigationItem.setLeftBarButton(UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, style: .plain, target: strongSelf, action: #selector(strongSelf.editingCancelPressed)), animated: true) + } else { + (strongSelf.controller?.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.3, curve: .linear)) + strongSelf.state = strongSelf.state.withIsEditing(true) + var updateOnCompletion = false + if strongSelf.headerNode.isAvatarExpanded { + updateOnCompletion = true + strongSelf.headerNode.skipCollapseCompletion = true + strongSelf.headerNode.avatarListNode.avatarContainerNode.canAttachVideo = false + strongSelf.headerNode.editingContentNode.avatarNode.canAttachVideo = false + strongSelf.headerNode.avatarListNode.listContainerNode.isCollapsing = true + strongSelf.headerNode.updateIsAvatarExpanded(false, transition: .immediate) + strongSelf.updateNavigationExpansionPresentation(isExpanded: false, animated: true) + } + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.scrollNode.view.setContentOffset(CGPoint(), animated: false) + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + UIView.transition(with: strongSelf.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { + }, completion: { _ in + if updateOnCompletion { + strongSelf.headerNode.skipCollapseCompletion = false + strongSelf.headerNode.avatarListNode.listContainerNode.isCollapsing = false + strongSelf.headerNode.avatarListNode.avatarContainerNode.canAttachVideo = true + strongSelf.headerNode.editingContentNode.avatarNode.canAttachVideo = true + strongSelf.headerNode.editingContentNode.avatarNode.reset() + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + } + }) + strongSelf.controller?.navigationItem.setLeftBarButton(UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, style: .plain, target: strongSelf, action: #selector(strongSelf.editingCancelPressed)), animated: true) + } case .done, .cancel: strongSelf.view.endEditing(true) if case .done = key { @@ -3207,7 +3273,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate strongSelf.controller?.present(emojiStatusSelectionController, in: .window(.root)) } } else { - screenData = peerInfoScreenData(context: context, peerId: peerId, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, isSettings: self.isSettings, hintGroupInCommon: hintGroupInCommon, existingRequestsContext: requestsContext) + screenData = peerInfoScreenData(context: context, peerId: peerId, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, isSettings: self.isSettings, hintGroupInCommon: hintGroupInCommon, existingRequestsContext: requestsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder) self.headerNode.displayPremiumIntro = { [weak self] sourceView, peerStatus, emojiStatusFileAndPack, white in guard let strongSelf = self else { @@ -4085,8 +4151,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate return .single(items) } - let allHeaderButtons = Set(peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: strongSelf.isOpenedFromChat, isExpanded: false, videoCallsEnabled: strongSelf.videoCallsEnabled, isSecretChat: strongSelf.peerId.namespace == Namespaces.Peer.SecretChat, isContact: strongSelf.data?.isContact ?? false)) - let headerButtons = Set(peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: strongSelf.isOpenedFromChat, isExpanded: true, videoCallsEnabled: strongSelf.videoCallsEnabled, isSecretChat: strongSelf.peerId.namespace == Namespaces.Peer.SecretChat, isContact: strongSelf.data?.isContact ?? false)) + let allHeaderButtons = Set(peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: strongSelf.isOpenedFromChat, isExpanded: false, videoCallsEnabled: strongSelf.videoCallsEnabled, isSecretChat: strongSelf.peerId.namespace == Namespaces.Peer.SecretChat, isContact: strongSelf.data?.isContact ?? false, threadInfo: data.threadInfo)) + let headerButtons = Set(peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: strongSelf.isOpenedFromChat, isExpanded: true, videoCallsEnabled: strongSelf.videoCallsEnabled, isSecretChat: strongSelf.peerId.namespace == Namespaces.Peer.SecretChat, isContact: strongSelf.data?.isContact ?? false, threadInfo: strongSelf.data?.threadInfo)) let filteredButtons = allHeaderButtons.subtracting(headerButtons) @@ -4676,18 +4742,32 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate private func openChatWithMessageSearch() { if let navigationController = (self.controller?.navigationController as? NavigationController) { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(id: self.peerId), keepStack: self.nearbyPeerDistance != nil ? .always : .default, activateMessageSearch: (.everything, ""), peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in - if let strongSelf = self, strongSelf.nearbyPeerDistance != nil { - var viewControllers = navigationController.viewControllers - viewControllers = viewControllers.filter { controller in - if controller is PeerInfoScreen { - return false - } - return true - } - navigationController.setViewControllers(viewControllers, animated: false) + if case let .replyThread(currentMessage) = self.chatLocation, let current = navigationController.viewControllers.first(where: { controller in + if let controller = controller as? ChatControllerImpl, case let .replyThread(message) = controller.chatLocation, message.messageId == currentMessage.messageId { + return true } - })) + return false + }) as? ChatControllerImpl { + var viewControllers = navigationController.viewControllers + if let index = viewControllers.firstIndex(of: current) { + viewControllers.removeSubrange(index + 1 ..< viewControllers.count) + } + navigationController.setViewControllers(viewControllers, animated: true) + current.activateSearch() + } else { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(id: self.peerId), keepStack: self.nearbyPeerDistance != nil ? .always : .default, activateMessageSearch: (.everything, ""), peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in + if let strongSelf = self, strongSelf.nearbyPeerDistance != nil { + var viewControllers = navigationController.viewControllers + viewControllers = viewControllers.filter { controller in + if controller is PeerInfoScreen { + return false + } + return true + } + navigationController.setViewControllers(viewControllers, animated: false) + } + })) + } } } @@ -4917,7 +4997,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } private func openUsername(value: String) { - let shareController = ShareController(context: self.context, subject: .url("https://t.me/\(value)"), updatedPresentationData: self.controller?.updatedPresentationData) + let url: String + if value.hasPrefix("https://") { + url = value + } else { + url = "https://t.me/\(value)" + } + + let shareController = ShareController(context: self.context, subject: .url(url), updatedPresentationData: self.controller?.updatedPresentationData) shareController.completed = { [weak self] peerIds in guard let strongSelf = self else { return @@ -6078,10 +6165,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate return nil } })) - case .link: - if let addressName = peer.addressName { - let text: String - let content: UndoOverlayContent + case let .link(customLink): + let text: String + let content: UndoOverlayContent + if let customLink = customLink { + text = customLink + content = .linkCopied(text: self.presentationData.strings.Conversation_LinkCopied) + } else if let addressName = peer.addressName { if peer is TelegramChannel { text = "https://t.me/\(addressName)" content = .linkCopied(text: self.presentationData.strings.Conversation_LinkCopied) @@ -6089,20 +6179,24 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate text = "@" + addressName content = .copy(text: self.presentationData.strings.Conversation_UsernameCopied) } - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in - UIPasteboard.general.string = text - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - })]) - controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in - if let controller = self?.controller, let sourceNode = sourceNode { - return (sourceNode, sourceNode.bounds.insetBy(dx: 0.0, dy: 2.0), controller.displayNode, controller.view.bounds) - } else { - return nil - } - })) + } else { + text = "https://t.me/@id\(peer.id.id._internalGetInt64Value())" + content = .linkCopied(text: self.presentationData.strings.Conversation_LinkCopied) } + + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in + UIPasteboard.general.string = text + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })]) + controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in + if let controller = self?.controller, let sourceNode = sourceNode { + return (sourceNode, sourceNode.bounds.insetBy(dx: 0.0, dy: 2.0), controller.displayNode, controller.view.bounds) + } else { + return nil + } + })) } } @@ -6597,8 +6691,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate guard let data = self.data, let peer = data.peer, let controller = self.controller else { return } + + var threadId: Int64 = 0 + if case let .replyThread(message) = self.chatLocation { + threadId = Int64(message.messageId.id) + } - controller.present(ChatQrCodeScreen(context: self.context, subject: .peer(peer)), in: .window(.root)) + controller.present(ChatQrCodeScreen(context: self.context, subject: .peer(peer: peer, threadId: threadId)), in: .window(.root)) } fileprivate func openSettings(section: PeerInfoSettingsSection) { @@ -7566,7 +7665,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } let headerInset = sectionInset - var headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, transition: transition, additive: additive) + var headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, threadInfo: self.data?.threadInfo, notificationSettings: self.data?.notificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, transition: transition, additive: additive) if !self.isSettings && !self.state.isEditing { headerHeight += 71.0 } @@ -7586,7 +7685,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate insets.left += sectionInset insets.right += sectionInset - let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages) + let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, chatLocation: self.chatLocation) contentHeight += headerHeight if !(self.isSettings && self.state.isEditing) { @@ -7926,7 +8025,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } let headerInset = sectionInset - let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, transition: transition, additive: additive) + let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, threadInfo: self.data?.threadInfo, notificationSettings: self.data?.notificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, transition: transition, additive: additive) } let paneAreaExpansionDistance: CGFloat = 32.0 @@ -8206,6 +8305,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private let isSettings: Bool private let hintGroupInCommon: PeerId? private weak var requestsContext: PeerInvitationImportersContext? + private let chatLocation: ChatLocation + private let chatLocationContextHolder = Atomic(value: nil) fileprivate var presentationData: PresentationData private var presentationDataDisposable: Disposable? @@ -8240,7 +8341,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool = false, hintGroupInCommon: PeerId? = nil, requestsContext: PeerInvitationImportersContext? = nil) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool = false, hintGroupInCommon: PeerId? = nil, requestsContext: PeerInvitationImportersContext? = nil, forumTopicThread: ChatReplyThreadMessage? = nil) { self.context = context self.updatedPresentationData = updatedPresentationData self.peerId = peerId @@ -8253,6 +8354,12 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.hintGroupInCommon = hintGroupInCommon self.requestsContext = requestsContext + if let forumTopicThread = forumTopicThread { + self.chatLocation = .replyThread(message: forumTopicThread) + } else { + self.chatLocation = .peer(id: peerId) + } + self.presentationData = updatedPresentationData?.0 ?? context.sharedContext.currentPresentationData.with { $0 } let baseNavigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) @@ -8436,33 +8543,35 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) } - - self.navigationBar?.makeCustomTransitionNode = { [weak self] other, isInteractive in - guard let strongSelf = self else { + + if case .peer = self.chatLocation { + self.navigationBar?.makeCustomTransitionNode = { [weak self] other, isInteractive in + guard let strongSelf = self else { + return nil + } + if strongSelf.navigationItem.leftBarButtonItem != nil { + return nil + } + if other.item?.leftBarButtonItem != nil { + return nil + } + if strongSelf.controllerNode.scrollNode.view.contentOffset.y > .ulpOfOne { + return nil + } + if isInteractive && strongSelf.controllerNode.headerNode.isAvatarExpanded { + return nil + } + if other.contentNode != nil { + return nil + } + if let allowsCustomTransition = other.allowsCustomTransition, !allowsCustomTransition() { + return nil + } + if let tag = other.userInfo as? PeerInfoNavigationSourceTag, tag.peerId == peerId { + return PeerInfoNavigationTransitionNode(screenNode: strongSelf.controllerNode, presentationData: strongSelf.presentationData, headerNode: strongSelf.controllerNode.headerNode) + } return nil } - if strongSelf.navigationItem.leftBarButtonItem != nil { - return nil - } - if other.item?.leftBarButtonItem != nil { - return nil - } - if strongSelf.controllerNode.scrollNode.view.contentOffset.y > .ulpOfOne { - return nil - } - if isInteractive && strongSelf.controllerNode.headerNode.isAvatarExpanded { - return nil - } - if other.contentNode != nil { - return nil - } - if let allowsCustomTransition = other.allowsCustomTransition, !allowsCustomTransition() { - return nil - } - if let tag = other.userInfo as? PeerInfoNavigationSourceTag, tag.peerId == peerId { - return PeerInfoNavigationTransitionNode(screenNode: strongSelf.controllerNode, presentationData: strongSelf.presentationData, headerNode: strongSelf.controllerNode.headerNode) - } - return nil } self.setStatusBarStyle(avatarInitiallyExpanded ? .White : self.presentationData.theme.rootController.statusBarStyle.style, animated: false) @@ -8534,7 +8643,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } override public func loadDisplayNode() { - self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, hintGroupInCommon: self.hintGroupInCommon, requestsContext: requestsContext) + self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder) self.controllerNode.accountsAndPeers.set(self.accountsAndPeers.get() |> map { $0.1 }) self.controllerNode.activeSessionsContextAndCount.set(self.activeSessionsContextAndCount.get()) self.cachedDataPromise.set(self.controllerNode.cachedDataPromise.get()) @@ -8933,7 +9042,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } let headerInset = sectionInset - topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, notificationSettings: self.screenNode.data?.notificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, transition: transition, additive: false) + topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadInfo: self.screenNode.data?.threadInfo, notificationSettings: self.screenNode.data?.notificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, transition: transition, additive: false) } let titleScale = (fraction * previousTitleNode.view.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.view.bounds.height diff --git a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift index e2d4f5c3f7..6006332083 100644 --- a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift @@ -761,6 +761,8 @@ final class PeerInfoGifPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDe private let context: AccountContext private let peerId: PeerId + private let chatLocation: ChatLocation + private let chatLocationContextHolder: Atomic private let chatControllerInteraction: ChatControllerInteraction private let contentType: ContentType @@ -809,9 +811,11 @@ final class PeerInfoGifPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDe return 0.0 } - init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, contentType: ContentType) { + init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, contentType: ContentType) { self.context = context self.peerId = peerId + self.chatLocation = chatLocation + self.chatLocationContextHolder = chatLocationContextHolder self.chatControllerInteraction = chatControllerInteraction self.contentType = contentType diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 772723cd6c..1e9b1d2ba6 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1468,8 +1468,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: context.engine.privacy.webSessions(), websitesOnly: false) } - public func makeChatQrCodeScreen(context: AccountContext, peer: Peer) -> ViewController { - return ChatQrCodeScreen(context: context, subject: .peer(peer)) + public func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?) -> ViewController { + return ChatQrCodeScreen(context: context, subject: .peer(peer: peer, threadId: threadId)) } public func makePrivacyAndSecurityController(context: AccountContext) -> ViewController { @@ -1532,7 +1532,14 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation if let _ = peer as? TelegramGroup { return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: []) } else if let _ = peer as? TelegramChannel { - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: []) + var forumTopicThread: ChatReplyThreadMessage? + switch mode { + case let .forumTopic(thread): + forumTopicThread = thread + default: + break + } + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], forumTopicThread: forumTopicThread) } else if peer is TelegramUser { var nearbyPeerDistance: Int32? var reactionSourceMessageId: MessageId? @@ -1549,6 +1556,8 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation hintGroupInCommon = id case let .reaction(messageId): reactionSourceMessageId = messageId + case .forumTopic: + break } return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, hintGroupInCommon: hintGroupInCommon) } else if peer is TelegramSecretChat { diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 5c3cfa3552..5897291836 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -73,7 +73,12 @@ public enum ParsedInternalPeerUrlParameter { } public enum ParsedInternalUrl { - case peerName(String, ParsedInternalPeerUrlParameter?) + public enum UrlPeerReference { + case name(String) + case id(PeerId) + } + + case peer(UrlPeerReference, ParsedInternalPeerUrlParameter?) case peerId(PeerId) case privateMessage(messageId: MessageId, threadId: Int32?, timecode: Double?) case stickerPack(name: String, type: StickerPackUrlType) @@ -193,9 +198,9 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { break } } - return .peerName(peerName, .attachBotStart(value, startAttach)) + return .peer(.name(peerName), .attachBotStart(value, startAttach)) } else if queryItem.name == "start" { - return .peerName(peerName, .botStart(value)) + return .peer(.name(peerName), .botStart(value)) } else if queryItem.name == "startgroup" { var botAdminRights: ResolvedBotAdminRights? for queryItem in queryItems { @@ -204,11 +209,11 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { break } } - return .peerName(peerName, .groupBotStart(value, botAdminRights)) + return .peer(.name(peerName), .groupBotStart(value, botAdminRights)) } else if queryItem.name == "game" { return nil } else if ["voicechat", "videochat", "livestream"].contains(queryItem.name) { - return .peerName(peerName, .voiceChat(value)) + return .peer(.name(peerName), .voiceChat(value)) } else if queryItem.name == "startattach" { var choose: String? for queryItem in queryItems { @@ -220,7 +225,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { return .startAttach(peerName, value, choose) } } else if ["voicechat", "videochat", "livestream"].contains(queryItem.name) { - return .peerName(peerName, .voiceChat(nil)) + return .peer(.name(peerName), .voiceChat(nil)) } else if queryItem.name == "startattach" { var choose: String? for queryItem in queryItems { @@ -238,7 +243,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { break } } - return .peerName(peerName, .groupBotStart("", botAdminRights)) + return .peer(.name(peerName), .groupBotStart("", botAdminRights)) } } } @@ -269,7 +274,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { let component = pathComponents[0].replacingOccurrences(of: "%24", with: "$") return .invoice(component) } - return .peerName(peerName, nil) + return .peer(.name(peerName), nil) } else if pathComponents.count == 2 || pathComponents.count == 3 { if pathComponents[0] == "addstickers" { return .stickerPack(name: pathComponents[1], type: .stickers) @@ -408,7 +413,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { - if queryItem.name == "thread" { + if queryItem.name == "thread" || queryItem.name == "topic" { if let intValue = Int32(value) { threadId = intValue } @@ -424,6 +429,29 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } else { return nil } + } else if pathComponents.count == 2 && pathComponents[0] == "c" { + if let channelId = Int64(pathComponents[1]) { + var threadId: Int32? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "thread" || queryItem.name == "topic" { + if let intValue = Int32(value) { + threadId = intValue + } + } + } + } + } + + if let threadId = threadId { + return .peer(.id(PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))), .replyThread(threadId, threadId)) + } else { + return nil + } + } else { + return nil + } } else if let value = Int32(pathComponents[1]) { var threadId: Int32? var commentId: Int32? @@ -431,7 +459,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { - if queryItem.name == "thread" { + if queryItem.name == "thread" || queryItem.name == "topic" { if let intValue = Int32(value) { threadId = intValue } @@ -448,11 +476,11 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } } if let threadId = threadId { - return .peerName(peerName, .replyThread(threadId, value)) + return .peer(.name(peerName), .replyThread(threadId, value)) } else if let commentId = commentId { - return .peerName(peerName, .replyThread(value, commentId)) + return .peer(.name(peerName), .replyThread(value, commentId)) } else { - return .peerName(peerName, .channelMessage(value, timecode)) + return .peer(.name(peerName), .channelMessage(value, timecode)) } } else { return nil @@ -489,12 +517,36 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .single(.peer(nil, .info)) } } - case let .peerName(name, parameter): - return context.engine.peers.resolvePeerByName(name: name) - |> take(1) - |> mapToSignal { peer -> Signal in - return .single(peer?._asPeer()) + case let .peer(reference, parameter): + let resolvedPeer: Signal + switch reference { + case let .name(name): + resolvedPeer = context.engine.peers.resolvePeerByName(name: name) + |> take(1) + |> mapToSignal { peer -> Signal in + return .single(peer?._asPeer()) + } + case let .id(id): + if id.namespace == Namespaces.Peer.CloudChannel { + resolvedPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: id)) + |> mapToSignal { peer -> Signal in + let foundPeer: Signal + if let peer = peer { + foundPeer = .single(peer._asPeer()) + } else { + foundPeer = TelegramEngine(account: context.account).peers.findChannelById(channelId: id.id._internalGetInt64Value()) + |> map { peer -> Peer? in + return peer?._asPeer() + } + } + return foundPeer + } + } else { + resolvedPeer = .single(nil) + } } + + return resolvedPeer |> mapToSignal { peer -> Signal in if let peer = peer { if let parameter = parameter {