diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index f90c1d0db1..22bd0501b8 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -552,7 +552,42 @@ public final class NavigateToChatControllerParams { public let customChatNavigationStack: [EnginePeer.Id]? public let skipAgeVerification: Bool - public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: NavigateToChatControllerParams.ReportReason? = nil, animated: Bool = true, forceAnimatedScroll: Bool = false, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? = nil, completion: @escaping (ChatController) -> Void = { _ in }, chatListCompletion: @escaping (ChatListController) -> Void = { _ in }, forceOpenChat: Bool = false, customChatNavigationStack: [EnginePeer.Id]? = nil, skipAgeVerification: Bool = false) { + public init( + navigationController: NavigationController, + chatController: ChatController? = nil, + context: AccountContext, + chatLocation: Location, + chatLocationContextHolder: Atomic = Atomic(value: nil), + subject: ChatControllerSubject? = nil, + botStart: ChatControllerInitialBotStart? = nil, + attachBotStart: ChatControllerInitialAttachBotStart? = nil, + botAppStart: ChatControllerInitialBotAppStart? = nil, + updateTextInputState: ChatTextInputState? = nil, + activateInput: ChatControllerActivateInput? = nil, + keepStack: NavigateToChatKeepStack = .default, + useExisting: Bool = true, + useBackAnimation: Bool = false, + purposefulAction: (() -> Void)? = nil, + scrollToEndIfExists: Bool = false, + activateMessageSearch: (ChatSearchDomain, String)? = nil, + peekData: ChatPeekTimeout? = nil, + peerNearbyData: ChatPeerNearbyData? = nil, + reportReason: NavigateToChatControllerParams.ReportReason? = nil, + animated: Bool = true, + forceAnimatedScroll: Bool = false, + options: NavigationAnimationOptions = [], + parentGroupId: PeerGroupId? = nil, + chatListFilter: Int32? = nil, + chatNavigationStack: [ChatNavigationStackItem] = [], + changeColors: Bool = false, + setupController: @escaping (ChatController) -> Void = { _ in }, + pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? = nil, + completion: @escaping (ChatController) -> Void = { _ in }, + chatListCompletion: @escaping (ChatListController) -> Void = { _ in }, + forceOpenChat: Bool = false, + customChatNavigationStack: [EnginePeer.Id]? = nil, + skipAgeVerification: Bool = false + ) { self.navigationController = navigationController self.chatController = chatController self.chatLocationContextHolder = chatLocationContextHolder diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 3fc94c1c06..da7e2d67f2 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -800,6 +800,7 @@ public enum ChatControllerSubject: Equatable { case pinnedMessages(id: EngineMessage.Id?) case messageOptions(peerIds: [EnginePeer.Id], ids: [EngineMessage.Id], info: MessageOptionsInfo) case customChatContents(contents: ChatCustomContentsProtocol) + case botForumThread(forumId: EnginePeer.Id, threadId: Int64) public static func ==(lhs: ChatControllerSubject, rhs: ChatControllerSubject) -> Bool { switch lhs { @@ -833,6 +834,12 @@ public enum ChatControllerSubject: Equatable { } else { return false } + case let .botForumThread(forumId, threadId): + if case .botForumThread(forumId, threadId) = rhs { + return true + } else { + return false + } } } diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 095e9a6bd8..de800066d2 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -587,7 +587,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } } -public func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: Int64, isPinned: Bool?, isClosed: Bool?, chatListController: ViewController?, joined: Bool, canSelect: Bool, customEdit: ((ContextController) -> Void)? = nil, customPinUnpin: ((ContextController) -> Void)? = nil, reorder: (() -> Void)? = nil) -> Signal<[ContextMenuItem], NoError> { +public func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: Int64, isPinned: Bool?, isClosed: Bool?, chatListController: ViewController?, joined: Bool, canSelect: Bool, customEdit: ((ContextController) -> Void)? = nil, customPinUnpin: ((ContextController) -> Void)? = nil, reorder: (() -> Void)? = nil, onDeleted: (() -> Void)? = nil) -> Signal<[ContextMenuItem], NoError> { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) let strings = presentationData.strings @@ -914,32 +914,33 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, thr }))) } if channel.hasPermission(.deleteAllMessages) { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak chatListController] _, f in - f(.default) - - if let chatListController = chatListController as? ChatListControllerImpl { - chatListController.deletePeerThread(peerId: peerId, threadId: threadId) - } else if let chatListController { - let actionSheet = ActionSheetController(presentationData: presentationData) - var items: [ActionSheetItem] = [] - - items.append(ActionSheetTextItem(title: presentationData.strings.ChatList_DeleteTopicConfirmationText, parseMarkdown: true)) - items.append(ActionSheetButtonItem(title: presentationData.strings.ChatList_DeleteTopicConfirmationAction, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - let _ = context.engine.peers.removeForumChannelThread(id: peerId, threadId: threadId).startStandalone(completed: { - }) - })) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak chatListController] c, _ in + c?.dismiss(completion: { + if let chatListController = chatListController as? ChatListControllerImpl { + chatListController.deletePeerThread(peerId: peerId, threadId: threadId) + } else if let chatListController { + let actionSheet = ActionSheetController(presentationData: presentationData) + var items: [ActionSheetItem] = [] + + items.append(ActionSheetTextItem(title: presentationData.strings.ChatList_DeleteTopicConfirmationText, parseMarkdown: true)) + items.append(ActionSheetButtonItem(title: presentationData.strings.ChatList_DeleteTopicConfirmationAction, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + onDeleted?() + let _ = context.engine.peers.removeForumChannelThread(id: peerId, threadId: threadId).startStandalone(completed: { }) + })) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) ]) - ]) - chatListController.present(actionSheet, in: .window(.root)) - } + chatListController.present(actionSheet, in: .window(.root)) + } + }) }))) } } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index c7434be7e4..3a844f1e90 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1027,144 +1027,169 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } self.chatListDisplayNode.mainContainerNode.peerSelected = { [weak self] peer, threadId, animated, activateInput, promoInfo in - guard let self else { - return - } - - var forumSourcePeer: Signal = .single(nil) - if case let .savedMessagesChats(peerId) = self.location, peerId != self.context.account.peerId { - forumSourcePeer = self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) - ) - } - - let _ = (combineLatest(queue: .mainQueue(), - self.context.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peer.id)]) - |> take(1), - forumSourcePeer - ) - |> deliverOnMainQueue).start(next: { [weak self] combinedView, forumSourcePeer in - guard let self, let cachedDataView = combinedView.views[.cachedPeerData(peerId: peer.id)] as? CachedPeerDataView else { - return - } - guard let navigationController = self.navigationController as? NavigationController else { + Task { @MainActor [weak self] in + guard let self else { return } - var peer = peer var threadId = threadId - if let forumSourcePeer { - threadId = peer.id.toInt64() - peer = forumSourcePeer + var subject: ChatControllerSubject? + if let threadIdValue = threadId, case let .user(user) = peer { + threadId = nil + + if case let .known(linkedForumId) = await self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.LinkedBotForumPeerId(id: user.id) + ).get(), let linkedForumId { + subject = .botForumThread(forumId: linkedForumId, threadId: threadIdValue) + } + subject = nil } - var scrollToEndIfExists = false - if let layout = self.validLayout, case .regular = layout.metrics.widthClass { - scrollToEndIfExists = true + var forumSourcePeer: Signal = .single(nil) + if case let .savedMessagesChats(peerId) = self.location, peerId != self.context.account.peerId { + forumSourcePeer = self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ) } - var openAsInlineForum = true - - if case let .channel(channel) = peer, channel.flags.contains(.isMonoforum) { - openAsInlineForum = false - } else if case let .channel(channel) = peer, channel.flags.contains(.displayForumAsTabs) { - openAsInlineForum = false - } else { - if let cachedData = cachedDataView.cachedPeerData as? CachedChannelData, case let .known(viewForumAsMessages) = cachedData.viewForumAsMessages, viewForumAsMessages { + let _ = (combineLatest(queue: .mainQueue(), + self.context.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peer.id)]) + |> take(1), + forumSourcePeer + ) + |> deliverOnMainQueue).start(next: { [weak self] combinedView, forumSourcePeer in + guard let self, let cachedDataView = combinedView.views[.cachedPeerData(peerId: peer.id)] as? CachedPeerDataView else { + return + } + guard let navigationController = self.navigationController as? NavigationController else { + return + } + + var peer = peer + var threadId = threadId + if let forumSourcePeer { + threadId = peer.id.toInt64() + peer = forumSourcePeer + } + + var scrollToEndIfExists = false + if let layout = self.validLayout, case .regular = layout.metrics.widthClass { + scrollToEndIfExists = true + } + + var openAsInlineForum = true + + if case let .channel(channel) = peer, channel.flags.contains(.isMonoforum) { + openAsInlineForum = false + } else if case let .channel(channel) = peer, channel.flags.contains(.displayForumAsTabs) { openAsInlineForum = false - } - } - - if openAsInlineForum, case let .channel(channel) = peer, channel.isForum, threadId == nil { - self.chatListDisplayNode.clearHighlightAnimated(true) - if self.chatListDisplayNode.inlineStackContainerNode?.location == .forum(peerId: channel.id) { - self.setInlineChatList(location: nil) } else { - self.setInlineChatList(location: .forum(peerId: channel.id)) - } - return - } - - if case let .channel(channel) = peer, channel.isForumOrMonoForum, let threadId { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( - navigationController: navigationController, - context: self.context, - chatLocation: .replyThread(ChatReplyThreadMessage( - peerId: peer.id, - threadId: threadId, - channelMessageId: nil, - isChannelPost: false, - isForumPost: true, - isMonoforumPost: channel.isMonoForum, - maxMessage: nil, - maxReadIncomingMessageId: nil, - maxReadOutgoingMessageId: nil, - unreadCount: 0, - initialFilledHoles: IndexSet(), - initialAnchor: .automatic, - isNotAvailable: false - )), - subject: nil, - keepStack: .always - )) - - self.chatListDisplayNode.clearHighlightAnimated(true) - } else { - var navigationAnimationOptions: NavigationAnimationOptions = [] - var groupId: EngineChatList.Group = .root - if case let .chatList(groupIdValue) = self.location { - groupId = groupIdValue - if case .root = groupIdValue { - navigationAnimationOptions = .removeOnMasterDetails + if let cachedData = cachedDataView.cachedPeerData as? CachedChannelData, case let .known(viewForumAsMessages) = cachedData.viewForumAsMessages, viewForumAsMessages { + openAsInlineForum = false } } - let chatLocation: NavigateToChatControllerParams.Location - chatLocation = .peer(peer) - - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, activateInput: (activateInput && !peer.isDeleted) ? .text : nil, scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: navigationAnimationOptions, parentGroupId: groupId._asGroup(), chatListFilter: self.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id, completion: { [weak self] controller in - guard let self else { - return + if openAsInlineForum, case let .channel(channel) = peer, channel.isForum, threadId == nil { + self.chatListDisplayNode.clearHighlightAnimated(true) + if self.chatListDisplayNode.inlineStackContainerNode?.location == .forum(peerId: channel.id) { + self.setInlineChatList(location: nil) + } else { + self.setInlineChatList(location: .forum(peerId: channel.id)) } - self.chatListDisplayNode.mainContainerNode.currentItemNode.clearHighlightAnimated(true) + return + } + + if case let .channel(channel) = peer, channel.isForumOrMonoForum, let threadId { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: self.context, + chatLocation: .replyThread(ChatReplyThreadMessage( + peerId: peer.id, + threadId: threadId, + channelMessageId: nil, + isChannelPost: false, + isForumPost: true, + isMonoforumPost: channel.isMonoForum, + maxMessage: nil, + maxReadIncomingMessageId: nil, + maxReadOutgoingMessageId: nil, + unreadCount: 0, + initialFilledHoles: IndexSet(), + initialAnchor: .automatic, + isNotAvailable: false + )), + subject: subject, + keepStack: .always + )) - if let promoInfo = promoInfo { - switch promoInfo { - case .proxy: - let _ = (ApplicationSpecificNotice.getProxyAdsAcknowledgment(accountManager: self.context.sharedContext.accountManager) - |> deliverOnMainQueue).startStandalone(next: { [weak self] value in - guard let self else { - return - } - if !value { - controller.displayPromoAnnouncement(text: self.presentationData.strings.DialogList_AdNoticeAlert) - let _ = ApplicationSpecificNotice.setProxyAdsAcknowledgment(accountManager: self.context.sharedContext.accountManager).startStandalone() - } - }) - case let .psa(type, _): - let _ = (ApplicationSpecificNotice.getPsaAcknowledgment(accountManager: self.context.sharedContext.accountManager, peerId: peer.id) - |> deliverOnMainQueue).startStandalone(next: { [weak self] value in - guard let self else { - return - } - if !value { - var text = self.presentationData.strings.ChatList_GenericPsaAlert - let key = "ChatList.PsaAlert.\(type)" - if let string = self.presentationData.strings.primaryComponent.dict[key] { - text = string - } else if let string = self.presentationData.strings.secondaryComponent?.dict[key] { - text = string - } - - controller.displayPromoAnnouncement(text: text) - let _ = ApplicationSpecificNotice.setPsaAcknowledgment(accountManager: self.context.sharedContext.accountManager, peerId: peer.id).startStandalone() - } - }) + self.chatListDisplayNode.clearHighlightAnimated(true) + } else { + var navigationAnimationOptions: NavigationAnimationOptions = [] + var groupId: EngineChatList.Group = .root + if case let .chatList(groupIdValue) = self.location { + groupId = groupIdValue + if case .root = groupIdValue { + navigationAnimationOptions = .removeOnMasterDetails } } - })) - } - }) + + let chatLocation: NavigateToChatControllerParams.Location + chatLocation = .peer(peer) + + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: self.context, + chatLocation: chatLocation, + subject: subject, + activateInput: (activateInput && !peer.isDeleted) ? .text : nil, + scrollToEndIfExists: scrollToEndIfExists, + animated: !scrollToEndIfExists, + options: navigationAnimationOptions, + parentGroupId: groupId._asGroup(), + chatListFilter: self.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id, completion: { [weak self] controller in + guard let self else { + return + } + self.chatListDisplayNode.mainContainerNode.currentItemNode.clearHighlightAnimated(true) + + if let promoInfo = promoInfo { + switch promoInfo { + case .proxy: + let _ = (ApplicationSpecificNotice.getProxyAdsAcknowledgment(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] value in + guard let self else { + return + } + if !value { + controller.displayPromoAnnouncement(text: self.presentationData.strings.DialogList_AdNoticeAlert) + let _ = ApplicationSpecificNotice.setProxyAdsAcknowledgment(accountManager: self.context.sharedContext.accountManager).startStandalone() + } + }) + case let .psa(type, _): + let _ = (ApplicationSpecificNotice.getPsaAcknowledgment(accountManager: self.context.sharedContext.accountManager, peerId: peer.id) + |> deliverOnMainQueue).startStandalone(next: { [weak self] value in + guard let self else { + return + } + if !value { + var text = self.presentationData.strings.ChatList_GenericPsaAlert + let key = "ChatList.PsaAlert.\(type)" + if let string = self.presentationData.strings.primaryComponent.dict[key] { + text = string + } else if let string = self.presentationData.strings.secondaryComponent?.dict[key] { + text = string + } + + controller.displayPromoAnnouncement(text: text) + let _ = ApplicationSpecificNotice.setPsaAcknowledgment(accountManager: self.context.sharedContext.accountManager, peerId: peer.id).startStandalone() + } + }) + } + } + })) + } + }) + } } self.chatListDisplayNode.mainContainerNode.groupSelected = { [weak self] groupId in diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index ad74d8d1b4..01bdf22f8b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -580,6 +580,9 @@ public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour { if threadId == nil, self.interaction.searchTextHighightState != nil, case let .channel(channel) = peerData.peer.peer, channel.isForumOrMonoForum { threadId = message.threadId } + if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), let forumTopicData = peerData.forumTopicData { + threadId = forumTopicData.id + } self.interaction.messageSelected(peer, threadId, message, peerData.promoInfo) } else if let peer = peerData.peer.peer { self.interaction.peerSelected(peer, nil, nil, peerData.promoInfo, false) @@ -2509,6 +2512,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { forumThread = (threadInfo.id, threadInfo.info.title, threadInfo.info.icon, threadInfo.info.iconColor, nil, false) } } + if let forumTopicData, forumThread == nil, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum) { + forumThread = (forumTopicData.id, forumTopicData.title, forumTopicData.iconFileId, forumTopicData.iconColor, forumTopicData.threadPeer, forumTopicData.isUnread) + } let messageText: String if let currentChatListText = currentChatListText, currentChatListText.0 == text { @@ -3421,7 +3427,12 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } } else if forumThread != nil || !topForumTopicItems.isEmpty { if let forumThread = forumThread { - isFirstForumThreadSelectable = forumThread.isUnread + if case let .peer(peer) = item.content, case .user = peer.peer.chatMainPeer { + isFirstForumThreadSelectable = false + } else { + isFirstForumThreadSelectable = forumThread.isUnread + } + forumThreads.append((id: forumThread.id, threadPeer: forumThread.threadPeer, title: NSAttributedString(string: forumThread.title, font: textFont, textColor: forumThread.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: forumThread.iconId, iconColor: forumThread.iconColor)) } for topicItem in topForumTopicItems { @@ -3475,7 +3486,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { textMaxWidth -= 18.0 } - let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: (authorAttributedString == nil && itemTags.isEmpty) ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: textMaxWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: (authorAttributedString == nil && itemTags.isEmpty && forumThread == nil) ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: textMaxWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) let maxTitleLines: Int switch item.index { diff --git a/submodules/Postbox/Sources/ChatListTable.swift b/submodules/Postbox/Sources/ChatListTable.swift index 7fcc985468..3d8f9f4787 100644 --- a/submodules/Postbox/Sources/ChatListTable.swift +++ b/submodules/Postbox/Sources/ChatListTable.swift @@ -373,7 +373,7 @@ final class ChatListTable: Table { return result } - func replay(historyOperationsByPeerId: [PeerId: [MessageHistoryOperation]], updatedPeerChatListEmbeddedStates: Set, updatedChatListInclusions: [PeerId: PeerChatListInclusion], messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, operations: inout [PeerGroupId: [ChatListOperation]]) { + func replay(postbox: PostboxImpl, historyOperationsByPeerId: [PeerId: [MessageHistoryOperation]], updatedPeerChatListEmbeddedStates: Set, updatedPeers: [PeerId: Peer], updatedChatListInclusions: [PeerId: PeerChatListInclusion], messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, operations: inout [PeerGroupId: [ChatListOperation]]) { var changedPeerIds = Set() for peerId in historyOperationsByPeerId.keys { changedPeerIds.insert(peerId) @@ -384,6 +384,12 @@ final class ChatListTable: Table { for peerId in updatedChatListInclusions.keys { changedPeerIds.insert(peerId) } + /*for peerId in updatedPeers.keys { + changedPeerIds.insert(peerId) + if let peer = postbox.peerTable.get(peerId), let targetPeerId = self.seedConfiguration.chatListPeerMergeIntoTargetId(peer) { + changedPeerIds.insert(targetPeerId) + } + }*/ self.ensureInitialized(groupId: .root) @@ -396,8 +402,8 @@ final class ChatListTable: Table { let topMessage = messageHistoryTable.topIndex(peerId: peerId) let embeddedChatStateOverrideTimestamp = peerChatInterfaceStateTable.get(peerId)?.overrideChatTimestamp - let rawTopMessageIndex: MessageIndex? - let topMessageIndex: MessageIndex? + var rawTopMessageIndex: MessageIndex? + var topMessageIndex: MessageIndex? if let topMessage = topMessage { var updatedTimestamp = topMessage.timestamp rawTopMessageIndex = MessageIndex(id: topMessage.id, timestamp: topMessage.timestamp) @@ -413,6 +419,22 @@ final class ChatListTable: Table { rawTopMessageIndex = nil } + /*for associatedId in postbox.reverseAssociatedPeerTable.get(peerId: peerId) { + if let peer = postbox.peerTable.get(associatedId), let targetPeerId = self.seedConfiguration.chatListPeerMergeIntoTargetId(peer), targetPeerId == peerId { + if let associatedTopMessage = messageHistoryTable.topIndex(peerId: associatedId) { + if let currentTopMessageIndex = topMessage { + if associatedTopMessage > currentTopMessageIndex { + topMessageIndex = associatedTopMessage + rawTopMessageIndex = associatedTopMessage + } + } else { + topMessageIndex = associatedTopMessage + rawTopMessageIndex = associatedTopMessage + } + } + } + }*/ + var updatedIndex = self.indexTable.setTopMessageIndex(peerId: peerId, index: topMessageIndex) if let updatedInclusion = updatedChatListInclusions[peerId] { updatedIndex = self.indexTable.setInclusion(peerId: peerId, inclusion: updatedInclusion) diff --git a/submodules/Postbox/Sources/ChatListView.swift b/submodules/Postbox/Sources/ChatListView.swift index b98ec21e41..70165b0155 100644 --- a/submodules/Postbox/Sources/ChatListView.swift +++ b/submodules/Postbox/Sources/ChatListView.swift @@ -1040,19 +1040,69 @@ public final class ChatListView { self.groupId = mutableView.groupId var entries: [ChatListEntry] = [] + + //TODO:release + var linkedEntries: [PeerId: [MutableChatListEntry.MessageEntryData]] = [:] + for entry in mutableView.sampledState.entries { + guard case let .MessageEntry(entryData) = entry else { + continue + } + if let peer = entryData.renderedPeer.peer, peer.id.namespace._internalGetInt32Value() == 2, let associatedPeerId = peer.associatedPeerId, associatedPeerId.namespace._internalGetInt32Value() == 0 { + if linkedEntries[associatedPeerId] == nil { + linkedEntries[associatedPeerId] = [] + } + linkedEntries[associatedPeerId]?.append(entryData) + } + } + for entry in mutableView.sampledState.entries { switch entry { case let .MessageEntry(entryData): + if let peer = entryData.renderedPeer.peer, peer.id.namespace._internalGetInt32Value() == 2, let associatedPeerId = peer.associatedPeerId, associatedPeerId.namespace._internalGetInt32Value() == 0 { + continue + } + + //TODO:release + var index = entryData.index + var messages = entryData.messages + var readState = entryData.readState + var forumTopicData = entryData.displayAsRegularChat ? nil : entryData.forumTopicData + if let linkedEntries = linkedEntries[entryData.index.messageIndex.id.peerId] { + for entry in linkedEntries { + if entry.index > index { + index = ChatListIndex(pinningIndex: index.pinningIndex, messageIndex: MessageIndex(id: index.messageIndex.id, timestamp: entry.index.messageIndex.timestamp)) + messages = entry.messages + forumTopicData = entry.forumTopicData + } + if let entryReadState = entry.readState { + if var readStateValue = readState { + var states = readStateValue.state.states + for (namespace, state) in entryReadState.state.states { + if let index = states.firstIndex(where: { $0.0 == namespace }) { + states[index] = (namespace, states[index].1.withAddedCount(state.count)) + } else { + states.append((namespace, state)) + } + } + readStateValue.state = .init(states: states) + readState = readStateValue + } else { + readState = entryReadState + } + } + } + } + entries.append(.MessageEntry(ChatListEntry.MessageEntryData( - index: entryData.index, - messages: entryData.messages, - readState: entryData.readState, + index: index, + messages: messages, + readState: readState, isRemovedFromTotalUnreadCount: entryData.isRemovedFromTotalUnreadCount, embeddedInterfaceState: entryData.embeddedInterfaceState, renderedPeer: entryData.renderedPeer, presence: entryData.presence, summaryInfo: entryData.tagSummaryInfo, - forumTopicData: entryData.displayAsRegularChat ? nil : entryData.forumTopicData, + forumTopicData: forumTopicData, topForumTopics: entryData.displayAsRegularChat ? [] : entryData.topForumTopics, hasFailed: entryData.hasFailedMessages, isContact: entryData.isContact, diff --git a/submodules/Postbox/Sources/SeedConfiguration.swift b/submodules/Postbox/Sources/SeedConfiguration.swift index 859186b969..f0e120e525 100644 --- a/submodules/Postbox/Sources/SeedConfiguration.swift +++ b/submodules/Postbox/Sources/SeedConfiguration.swift @@ -81,6 +81,7 @@ public final class SeedConfiguration { public let automaticThreadIndexInfo: (PeerId, Int64) -> StoredMessageHistoryThreadInfo? public let customTagsFromAttributes: ([MessageAttribute]) -> [MemoryBuffer] public let displaySavedMessagesAsTopicListPreferencesKey: ValueBoxKey + public let chatListPeerMergeIntoTargetId: (Peer) -> PeerId? public init( globalMessageIdsPeerIdNamespaces: Set, @@ -111,7 +112,8 @@ public final class SeedConfiguration { isPeerUpgradeMessage: @escaping (Message) -> Bool, automaticThreadIndexInfo: @escaping (PeerId, Int64) -> StoredMessageHistoryThreadInfo?, customTagsFromAttributes: @escaping ([MessageAttribute]) -> [MemoryBuffer], - displaySavedMessagesAsTopicListPreferencesKey: ValueBoxKey + displaySavedMessagesAsTopicListPreferencesKey: ValueBoxKey, + chatListPeerMergeIntoTargetId: @escaping (Peer) -> PeerId? ) { self.globalMessageIdsPeerIdNamespaces = globalMessageIdsPeerIdNamespaces self.initializeChatListWithHole = initializeChatListWithHole @@ -138,5 +140,6 @@ public final class SeedConfiguration { self.automaticThreadIndexInfo = automaticThreadIndexInfo self.customTagsFromAttributes = customTagsFromAttributes self.displaySavedMessagesAsTopicListPreferencesKey = displaySavedMessagesAsTopicListPreferencesKey + self.chatListPeerMergeIntoTargetId = chatListPeerMergeIntoTargetId } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift index 065943289f..16110159eb 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift @@ -76,10 +76,14 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = { peerSummaryIsThreadBased: { peer, associatedPeer in if let channel = peer as? TelegramChannel { if channel.flags.contains(.isForum) { - if channel.flags.contains(.displayForumAsTabs) { - return (false, false) - } else { + if channel.linkedBotId != nil { return (true, false) + } else { + if channel.flags.contains(.displayForumAsTabs) { + return (false, false) + } else { + return (true, false) + } } } else if channel.flags.contains(.isMonoforum) { if let associatedPeer = associatedPeer as? TelegramChannel, associatedPeer.hasPermission(.manageDirect) { @@ -241,7 +245,13 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = { return result }, - displaySavedMessagesAsTopicListPreferencesKey: PreferencesKeys.displaySavedChatsAsTopics() + displaySavedMessagesAsTopicListPreferencesKey: PreferencesKeys.displaySavedChatsAsTopics(), + chatListPeerMergeIntoTargetId: { peer in + if let peer = peer as? TelegramChannel, let linkedBotId = peer.linkedBotId { + return linkedBotId + } + return nil + } ) }() diff --git a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift index 37a7531905..98a8c1e5e9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift @@ -26,13 +26,13 @@ public final class ChatAvatarNavigationNode: ASDisplayNode { private var context: AccountContext? private let containerNode: ContextControllerSourceNode - public let avatarNode: AvatarNode + public var avatarNode: AvatarNode private var avatarVideoNode: AvatarVideoNode? public private(set) var avatarStoryView: ComponentView? public var storyData: (hasUnseen: Bool, hasUnseenCloseFriends: Bool)? - public let statusView: ComponentView + public var statusView: ComponentView private var starView: StarView? private var cachedDataDisposable = MetaDisposable() @@ -119,6 +119,13 @@ public final class ChatAvatarNavigationNode: ASDisplayNode { public func setPeer(context: AccountContext, theme: PresentationTheme, peer: EnginePeer?, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, clipStyle: AvatarNodeClipStyle = .round, synchronousLoad: Bool = false, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), storeUnrounded: Bool = false) { self.context = context + + if let statusComponentView = self.statusView.view { + self.statusView = ComponentView() + statusComponentView.removeFromSuperview() + } + + self.avatarNode.isHidden = false self.avatarNode.setPeer(context: context, theme: theme, peer: peer, authorOfMessage: authorOfMessage, overrideImage: overrideImage, emptyColor: emptyColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: displayDimensions, storeUnrounded: storeUnrounded) if let peer, peer.isSubscription { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index 173aa77857..c0770c076b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -479,7 +479,7 @@ private final class ChatMessagePeerContentNode: ASDisplayNode { avatarIconContent = .topic(title: String(info.title.prefix(1)), color: info.iconColor, size: CGSize(width: 16.0, height: 16.0)) } } else { - avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicIcon(presentationData.theme.theme)?.withRenderingMode(.alwaysTemplate), tintColor: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: presentationData.theme.wallpaper)) + avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicTemplateIcon(presentationData.theme.theme), tintColor: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: presentationData.theme.wallpaper)) } let avatarIconComponent = EmojiStatusComponent( diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift index 1abefd9b5c..9736fd3195 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift @@ -45,12 +45,18 @@ public final class ChatSideTopicsPanel: Component { case top } + public enum Kind { + case forum + case monoforum + case botForum + } + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let location: Location let peerId: EnginePeer.Id - let isMonoforum: Bool + let kind: Kind let topicId: Int64? let controller: () -> ViewController? let togglePanel: () -> Void @@ -63,7 +69,7 @@ public final class ChatSideTopicsPanel: Component { strings: PresentationStrings, location: Location, peerId: EnginePeer.Id, - isMonoforum: Bool, + kind: Kind, topicId: Int64?, controller: @escaping () -> ViewController?, togglePanel: @escaping () -> Void, @@ -75,7 +81,7 @@ public final class ChatSideTopicsPanel: Component { self.strings = strings self.location = location self.peerId = peerId - self.isMonoforum = isMonoforum + self.kind = kind self.topicId = topicId self.controller = controller self.togglePanel = togglePanel @@ -99,7 +105,7 @@ public final class ChatSideTopicsPanel: Component { if lhs.peerId != rhs.peerId { return false } - if lhs.isMonoforum != rhs.isMonoforum { + if lhs.kind != rhs.kind { return false } if lhs.topicId != rhs.topicId { @@ -1144,12 +1150,14 @@ public final class ChatSideTopicsPanel: Component { private final class VerticalAllItemComponent: Component, AllItemComponent { let isSelected: Bool + let kind: ChatSideTopicsPanel.Kind let theme: PresentationTheme let strings: PresentationStrings let action: (() -> Void)? - init(isSelected: Bool, theme: PresentationTheme, strings: PresentationStrings, action: (() -> Void)?) { + init(isSelected: Bool, kind: ChatSideTopicsPanel.Kind, theme: PresentationTheme, strings: PresentationStrings, action: (() -> Void)?) { self.isSelected = isSelected + self.kind = kind self.theme = theme self.strings = strings self.action = action @@ -1162,6 +1170,9 @@ public final class ChatSideTopicsPanel: Component { if lhs.isSelected != rhs.isSelected { return false } + if lhs.kind != rhs.kind { + return false + } if lhs.theme !== rhs.theme { return false } @@ -1234,7 +1245,13 @@ public final class ChatSideTopicsPanel: Component { containerSize: CGSize(width: 100.0, height: 100.0) ) - let titleText: String = component.strings.Chat_InlineTopicMenu_AllTab + let titleText: String + if case .botForum = component.kind { + //TODO:localize + titleText = "General" + } else { + titleText = component.strings.Chat_InlineTopicMenu_AllTab + } let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -1284,12 +1301,14 @@ public final class ChatSideTopicsPanel: Component { private final class HorizontalAllItemComponent: Component, AllItemComponent { let isSelected: Bool + let kind: ChatSideTopicsPanel.Kind let theme: PresentationTheme let strings: PresentationStrings let action: (() -> Void)? - init(isSelected: Bool, theme: PresentationTheme, strings: PresentationStrings, action: (() -> Void)?) { + init(isSelected: Bool, kind: ChatSideTopicsPanel.Kind, theme: PresentationTheme, strings: PresentationStrings, action: (() -> Void)?) { self.isSelected = isSelected + self.kind = kind self.theme = theme self.strings = strings self.action = action @@ -1302,6 +1321,9 @@ public final class ChatSideTopicsPanel: Component { if lhs.isSelected != rhs.isSelected { return false } + if lhs.kind != rhs.kind { + return false + } if lhs.theme !== rhs.theme { return false } @@ -1361,7 +1383,13 @@ public final class ChatSideTopicsPanel: Component { let leftInset: CGFloat = 6.0 let rightInset: CGFloat = 12.0 - let titleText: String = component.strings.Chat_InlineTopicMenu_AllTab + let titleText: String + if case .botForum = component.kind { + //TODO:localize + titleText = "General" + } else { + titleText = component.strings.Chat_InlineTopicMenu_AllTab + } let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -1372,7 +1400,7 @@ public final class ChatSideTopicsPanel: Component { containerSize: CGSize(width: 400.0, height: 200.0) ) - let contentSize: CGFloat = leftInset + rightInset + titleSize.height + let contentSize: CGFloat = leftInset + rightInset + titleSize.width let size = CGSize(width: contentSize, height: availableSize.height) let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) @@ -1422,6 +1450,7 @@ public final class ChatSideTopicsPanel: Component { private var tabItemView: TabItemView? + private var peerId: EnginePeer.Id? private var rawItems: [Item] = [] private var reorderingItems: [Item]? private var resetReorderingOnNextUpdate: Bool = false @@ -1653,18 +1682,62 @@ public final class ChatSideTopicsPanel: Component { } if self.component == nil { - let threadListSignal: Signal = component.context.sharedContext.subscribeChatListData(context: component.context, location: component.isMonoforum ? .savedMessagesChats(peerId: component.peerId) : .forum(peerId: component.peerId)) + let threadListSignal: Signal<(EnginePeer.Id, EngineChatList), NoError> + + switch component.kind { + case .botForum: + let forumPeerId: Signal + if component.peerId.namespace == Namespaces.Peer.CloudUser { + forumPeerId = component.context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.LinkedBotForumPeerId(id: component.peerId) + ) + |> map { value -> EnginePeer.Id? in + if case let .known(value) = value { + return value + } else { + return nil + } + } + |> distinctUntilChanged + } else { + forumPeerId = .single(component.peerId) + } + + let defaultPeerId = component.peerId + threadListSignal = forumPeerId + |> mapToSignal { forumPeerId -> Signal<(EnginePeer.Id, EngineChatList), NoError> in + if let forumPeerId { + return component.context.sharedContext.subscribeChatListData(context: component.context, location: .forum(peerId: forumPeerId)) + |> map { value in + return (forumPeerId, value) + } + } else { + return .single((defaultPeerId, EngineChatList(items: [], groupItems: [], additionalItems: [], hasEarlier: false, hasLater: false, isLoading: false))) + } + } + default: + let defaultPeerId = component.peerId + threadListSignal = component.context.sharedContext.subscribeChatListData(context: component.context, location: component.kind == .monoforum ? .savedMessagesChats(peerId: component.peerId) : .forum(peerId: component.peerId)) + |> map { value in + return (defaultPeerId, value) + } + } self.itemsDisposable = (threadListSignal - |> deliverOnMainQueue).startStrict(next: { [weak self] chatList in - guard let self else { + |> deliverOnMainQueue).startStrict(next: { [weak self] peerId, chatList in + guard let self, let component = self.component else { return } + self.peerId = peerId + let wasEmpty = self.rawItems.isEmpty self.rawItems.removeAll() for item in chatList.items.reversed() { + if case .botForum = component.kind, case let .forum(topicId) = item.id, topicId == 1 { + continue + } self.rawItems.append(Item(item: item)) } @@ -1786,7 +1859,7 @@ public final class ChatSideTopicsPanel: Component { itemTransition = .immediate animateIn = true itemView = TabItemView(context: component.context, action: { [weak self] in - guard let self, let component = self.component else { + guard let self, let peerId = self.peerId, let component = self.component else { return } if self.isReordering { @@ -1806,7 +1879,7 @@ public final class ChatSideTopicsPanel: Component { } if threadIds != currentThreadIds { - let _ = component.context.engine.peers.setForumChannelPinnedTopics(id: component.peerId, threadIds: threadIds).startStandalone() + let _ = component.context.engine.peers.setForumChannelPinnedTopics(id: peerId, threadIds: threadIds).startStandalone() self.resetReorderingOnNextUpdate = true } else { self.reorderingItems = nil @@ -1880,6 +1953,7 @@ public final class ChatSideTopicsPanel: Component { id: ScrollId.all, component: AnyComponent(VerticalAllItemComponent( isSelected: component.topicId == nil, + kind: component.kind, theme: component.theme, strings: component.strings, action: { [weak self] in @@ -1895,6 +1969,7 @@ public final class ChatSideTopicsPanel: Component { id: ScrollId.all, component: AnyComponent(HorizontalAllItemComponent( isSelected: component.topicId == nil, + kind: component.kind, theme: component.theme, strings: component.strings, action: { [weak self] in @@ -1937,10 +2012,10 @@ public final class ChatSideTopicsPanel: Component { component.updateTopicId(topicId, direction) } var itemContextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)? - if !self.isReordering && component.isMonoforum { + if !self.isReordering, case .monoforum = component.kind { itemContextGesture = { [weak self] gesture, sourceNode in Task { @MainActor in - guard let self, let component = self.component else { + guard let self, let peerId = self.peerId, let component = self.component else { return } guard let controller = component.controller() else { @@ -1964,18 +2039,18 @@ public final class ChatSideTopicsPanel: Component { var items: [ContextMenuItem] = [] let threadInfo = await component.context.engine.data.get( - TelegramEngine.EngineData.Item.Messages.ThreadInfo(peerId: component.peerId, threadId: topicId) + TelegramEngine.EngineData.Item.Messages.ThreadInfo(peerId: peerId, threadId: topicId) ).get() if let threadInfo, threadInfo.isMessageFeeRemoved { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ReinstatePaidMessages, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in - guard let self, let component = self.component else { + guard let self, let peerId = self.peerId, let component = self.component else { return } c?.dismiss(completion: {}) - let _ = component.context.engine.peers.reinstateNoPaidMessagesException(scopePeerId: component.peerId, peerId: EnginePeer.Id(topicId)).startStandalone() + let _ = component.context.engine.peers.reinstateNoPaidMessagesException(scopePeerId: peerId, peerId: EnginePeer.Id(topicId)).startStandalone() }))) } @@ -2011,7 +2086,7 @@ public final class ChatSideTopicsPanel: Component { } } else if !self.isReordering { itemContextGesture = { [weak self] gesture, sourceNode in - guard let self, let component = self.component else { + guard let self, let peerId = self.peerId, let component = self.component else { return } guard let controller = component.controller() else { @@ -2043,7 +2118,7 @@ public final class ChatSideTopicsPanel: Component { let _ = (chatForumTopicMenuItems( context: component.context, - peerId: component.peerId, + peerId: peerId, threadId: topicId, isPinned: isPinned, isClosed: isClosed, @@ -2052,12 +2127,12 @@ public final class ChatSideTopicsPanel: Component { canSelect: false, customEdit: { [weak self] contextController in contextController.dismiss(completion: { - guard let self, let component = self.component, let threadData else { + guard let self, let peerId = self.peerId, let component = self.component, let threadData else { return } let editController = component.context.sharedContext.makeEditForumTopicScreen( context: component.context, - peerId: component.peerId, + peerId: peerId, threadId: topicId, threadInfo: threadData.info, isHidden: threadData.isHidden @@ -2066,7 +2141,7 @@ public final class ChatSideTopicsPanel: Component { }) }, customPinUnpin: { [weak self] contextController in - guard let self, let component = self.component else { + guard let self, let peerId = self.peerId, let component = self.component else { contextController.dismiss(completion: {}) return } @@ -2074,7 +2149,7 @@ public final class ChatSideTopicsPanel: Component { self.isTogglingPinnedItem = true self.dismissContextControllerOnNextUpdate = contextController - let _ = (component.context.engine.peers.toggleForumChannelTopicPinned(id: component.peerId, threadId: topicId) + let _ = (component.context.engine.peers.toggleForumChannelTopicPinned(id: peerId, threadId: topicId) |> deliverOnMainQueue).startStandalone(error: { [weak self, weak contextController] error in guard let self, let component = self.component else { contextController?.dismiss(completion: {}) @@ -2099,6 +2174,12 @@ public final class ChatSideTopicsPanel: Component { return } self.updateIsReordering(isReordering: true) + }, + onDeleted: { [weak self] in + guard let self, let component = self.component else { + return + } + component.updateTopicId(nil, false) } ) |> take(1) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index f87af616b9..989fe68571 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -1263,15 +1263,18 @@ final class PeerInfoHeaderNode: ASDisplayNode { subtitleColor = UIColor.white let statusText: String - statusText = peer.debugDisplayTitle + if let channel = peer as? TelegramChannel, channel.linkedBotId != nil { + statusText = " " + } else { + statusText = peer.debugDisplayTitle + subtitleIsButton = true + } subtitleStringText = statusText subtitleAttributes = MultiScaleTextState.Attributes(font: Font.semibold(16.0), color: subtitleColor) smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: .white, shadowColor: titleShadowColor) usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(16.0), color: .white)) - - subtitleIsButton = true let (maybePanelStatusData, _, _) = panelStatusData if let panelStatusData = maybePanelStatusData { @@ -1979,7 +1982,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.currentPendingStarRating = nil } - #if DEBUG + #if DEBUG && false if "".isEmpty { let starRating: TelegramStarRating diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 07cb8db0b5..1e01e43590 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -12680,7 +12680,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } else if peerInfoCanEdit(peer: self.data?.peer, chatLocation: self.chatLocation, threadData: self.data?.threadData, cachedData: self.data?.cachedData, isContact: self.data?.isContact) { rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false)) } - if let data = self.data, !data.isPremiumRequiredForStoryPosting || data.accountIsPremium, let channel = data.peer as? TelegramChannel, channel.hasPermission(.postStories) { + if let data = self.data, !data.isPremiumRequiredForStoryPosting || data.accountIsPremium, let channel = data.peer as? TelegramChannel, channel.hasPermission(.postStories), channel.linkedBotId == nil { rightNavigationButtons.insert(PeerInfoHeaderNavigationButtonSpec(key: .postStory, isForExpandedView: false), at: 0) } else if self.isMyProfile { rightNavigationButtons.insert(PeerInfoHeaderNavigationButtonSpec(key: .postStory, isForExpandedView: false), at: 0) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index ab54dc3d30..17c54b412f 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -166,6 +166,10 @@ extension ChatControllerImpl { if let historyNodeData = contentData.state.historyNodeData { self.updateChatLocationToOther(chatLocation: historyNodeData.chatLocation) return + } else if case let .botForumThread(_, threadId) = self.subject { + self.subject = nil + self.updateChatLocationThread(threadId: threadId) + return } apply({ [weak self, weak contentData] forceAnimation in diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift index 951d94b1f2..93263e13fa 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift @@ -123,6 +123,7 @@ import ChatEmptyNode import ChatMediaInputStickerGridItem import AdsInfoScreen import FaceScanScreen +import ForumCreateTopicScreen extension ChatControllerImpl { func openPeer(peer: EnginePeer?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: MessageReference?, fromReactionMessageId: MessageId? = nil, expandAvatar: Bool = false, peerTypes: ReplyMarkupButtonAction.PeerTypes? = nil, skipAgeVerification: Bool = false) { @@ -328,4 +329,106 @@ extension ChatControllerImpl { } }) } + + func openBotForumMoreMenu(sourceView: UIView, gesture: ContextGesture?) { + Task { @MainActor [weak self] in + guard let self, let peerId = self.chatLocation.peerId else { + return + } + guard let forumPeerId = await (self.context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.LinkedBotForumPeerId(id: peerId) + ) + |> map { value -> EnginePeer.Id? in + if case let .known(value) = value { + return value + } else { + return nil + } + }).get() else { + return + } + + let strings = self.presentationData.strings + + var items: [ContextMenuItem] = [] + + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Open Profile", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self, let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer else { + return + } + + guard let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + return + } + (self.navigationController as? NavigationController)?.pushViewController(controller) + }))) + + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: strings.Conversation_Search, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Search"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] action in + action.dismissWithResult(.default) + + self?.beginMessageSearch("") + }))) + + items.append(.action(ContextMenuActionItem(text: strings.Chat_CreateTopic, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] action in + guard let self else { + return + } + + action.dismissWithResult(.default) + + let controller = ForumCreateTopicScreen(context: self.context, peerId: forumPeerId, mode: .create) + controller.navigationPresentation = .modal + + controller.completion = { [weak self, weak controller] title, fileId, iconColor, _ in + controller?.isInProgress = true + controller?.view.endEditing(true) + + guard let self else { + return + } + + let _ = (self.context.engine.peers.createForumChannelTopic(id: forumPeerId, title: title, iconColor: iconColor, iconFileId: fileId) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] topicId in + guard let self else { + return + } + self.updateChatLocationThread(threadId: topicId) + controller?.dismiss() + }, error: { _ in + controller?.isInProgress = false + }) + } + self.push(controller) + }))) + + let presentationData = self.presentationData + + let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + self.presentInGlobalOverlay(contextController) + } + } +} + +private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceView: UIView + + init(controller: ViewController, sourceView: UIView) { + self.controller = controller + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) + } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 06afa6c5e5..68b9a055eb 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -247,7 +247,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let context: AccountContext public internal(set) var chatLocation: ChatLocation - public let subject: ChatControllerSubject? + public internal(set) var subject: ChatControllerSubject? var botStart: ChatControllerInitialBotStart? var attachBotStart: ChatControllerInitialAttachBotStart? @@ -5528,6 +5528,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if peerId == self.context.account.peerId { PeerInfoScreenImpl.openSavedMessagesMoreMenu(context: self.context, sourceController: self, isViewingAsTopics: false, sourceView: sourceNode.view, gesture: gesture) + } else if peerId.namespace == Namespaces.Peer.CloudUser { + self.openBotForumMoreMenu(sourceView: sourceNode.view, gesture: gesture) } else { ChatListControllerImpl.openMoreMenu(context: self.context, peerId: peerId, sourceController: self, isViewingAsTopics: false, sourceView: sourceNode.view, gesture: gesture) } @@ -8827,7 +8829,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - #if DEBUG + #if DEBUG && false let botForumId = await self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.LinkedBotForumPeerId(id: peerId) ).get() @@ -10114,90 +10116,124 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } public func updateChatLocationThread(threadId: Int64?, animationDirection: ChatControllerAnimateInnerChatSwitchDirection? = nil) { - if self.isUpdatingChatLocationThread { - return - } - guard let peerId = self.chatLocation.peerId else { - return - } - if self.chatLocation.threadId == threadId { - return - } - guard let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer else { - return - } - - self.saveInterfaceState() - - self.chatDisplayNode.dismissTextInput() - - let updatedChatLocation: ChatLocation - if let threadId { - var isMonoforum = false - if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) { - isMonoforum = true - } - - updatedChatLocation = .replyThread(message: ChatReplyThreadMessage( - peerId: peerId, - threadId: threadId, - channelMessageId: nil, - isChannelPost: false, - isForumPost: true, - isMonoforumPost: isMonoforum, - maxMessage: nil, - maxReadIncomingMessageId: nil, - maxReadOutgoingMessageId: nil, - unreadCount: 0, - initialFilledHoles: IndexSet(), - initialAnchor: .automatic, - isNotAvailable: false - )) - } else { - updatedChatLocation = .peer(id: peerId) - } - - let navigationSnapshot = self.chatTitleView?.prepareSnapshotState() - let avatarSnapshot = self.chatInfoNavigationButton?.buttonItem.customDisplayNode?.view.window != nil ? (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.prepareSnapshotState() : nil - - let chatLocationContextHolder = Atomic(value: nil) - let historyNode = self.chatDisplayNode.createHistoryNodeForChatLocation(chatLocation: updatedChatLocation, chatLocationContextHolder: chatLocationContextHolder) - self.isUpdatingChatLocationThread = true - self.reloadChatLocation(chatLocation: updatedChatLocation, chatLocationContextHolder: chatLocationContextHolder, historyNode: historyNode, apply: { [weak self, weak historyNode] apply in - guard let self, let historyNode else { + Task { @MainActor [weak self] in + guard let self else { return } - self.currentChatSwitchDirection = animationDirection - self.chatLocation = updatedChatLocation - historyNode.areContentAnimationsEnabled = true - self.chatDisplayNode.prepareSwitchToChatLocation(historyNode: historyNode, animationDirection: animationDirection) + if self.isUpdatingChatLocationThread { + return + } + guard let peerId = self.chatLocation.peerId else { + return + } + if self.chatLocation.threadId == threadId { + return + } + guard let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer else { + return + } - apply(true) + self.saveInterfaceState() - if let navigationSnapshot, let animationDirection { - let mappedAnimationDirection: ChatTitleView.AnimateFromSnapshotDirection - switch animationDirection { - case .up: - mappedAnimationDirection = .up - case .down: - mappedAnimationDirection = .down - case .left: - mappedAnimationDirection = .left - case .right: - mappedAnimationDirection = .right + self.chatDisplayNode.dismissTextInput() + + let updatedChatLocation: ChatLocation + if let threadId { + if let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum) { + guard case let .known(linkedForumId) = await self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.LinkedBotForumPeerId(id: user.id) + ).get(), let linkedForumId else { + return + } + + updatedChatLocation = .replyThread(message: ChatReplyThreadMessage( + peerId: linkedForumId, + threadId: threadId, + channelMessageId: nil, + isChannelPost: false, + isForumPost: true, + isMonoforumPost: false, + maxMessage: nil, + maxReadIncomingMessageId: nil, + maxReadOutgoingMessageId: nil, + unreadCount: 0, + initialFilledHoles: IndexSet(), + initialAnchor: .automatic, + isNotAvailable: false + )) + } else { + var isMonoforum = false + if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) { + isMonoforum = true + } + + updatedChatLocation = .replyThread(message: ChatReplyThreadMessage( + peerId: peerId, + threadId: threadId, + channelMessageId: nil, + isChannelPost: false, + isForumPost: true, + isMonoforumPost: isMonoforum, + maxMessage: nil, + maxReadIncomingMessageId: nil, + maxReadOutgoingMessageId: nil, + unreadCount: 0, + initialFilledHoles: IndexSet(), + initialAnchor: .automatic, + isNotAvailable: false + )) + } + } else { + if let channel = peer as? TelegramChannel, let linkedBotId = channel.linkedBotId { + updatedChatLocation = .peer(id: linkedBotId) + } else { + updatedChatLocation = .peer(id: peerId) + } + } + + let navigationSnapshot = self.chatTitleView?.prepareSnapshotState() + let avatarSnapshot = self.chatInfoNavigationButton?.buttonItem.customDisplayNode?.view.window != nil ? (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.prepareSnapshotState() : nil + + let chatLocationContextHolder = Atomic(value: nil) + let historyNode = self.chatDisplayNode.createHistoryNodeForChatLocation(chatLocation: updatedChatLocation, chatLocationContextHolder: chatLocationContextHolder) + self.isUpdatingChatLocationThread = true + self.reloadChatLocation(chatLocation: updatedChatLocation, chatLocationContextHolder: chatLocationContextHolder, historyNode: historyNode, apply: { [weak self, weak historyNode] apply in + guard let self, let historyNode else { + return } - self.chatTitleView?.animateFromSnapshot(navigationSnapshot, direction: mappedAnimationDirection) - } - - if let avatarSnapshot, self.chatInfoNavigationButton?.buttonItem.customDisplayNode?.view.window != nil { - (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.animateFromSnapshot(avatarSnapshot) - } - - self.currentChatSwitchDirection = nil - self.isUpdatingChatLocationThread = false - }) + self.currentChatSwitchDirection = animationDirection + self.chatLocation = updatedChatLocation + historyNode.areContentAnimationsEnabled = true + self.chatDisplayNode.prepareSwitchToChatLocation(historyNode: historyNode, animationDirection: animationDirection) + + apply(true) + + if let navigationSnapshot, let animationDirection { + let mappedAnimationDirection: ChatTitleView.AnimateFromSnapshotDirection + switch animationDirection { + case .up: + mappedAnimationDirection = .up + case .down: + mappedAnimationDirection = .down + case .left: + mappedAnimationDirection = .left + case .right: + mappedAnimationDirection = .right + } + + self.chatTitleView?.animateFromSnapshot(navigationSnapshot, direction: mappedAnimationDirection) + } + + if let avatarSnapshot, self.chatInfoNavigationButton?.buttonItem.customDisplayNode?.view.window != nil { + (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.animateFromSnapshot(avatarSnapshot) + } + + self.currentChatSwitchDirection = nil + self.isUpdatingChatLocationThread = false + }) + } } public var contentContainerNode: ASDisplayNode { diff --git a/submodules/TelegramUI/Sources/ChatControllerContentData.swift b/submodules/TelegramUI/Sources/ChatControllerContentData.swift index 48d7c52ec3..2a9f6bff7a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerContentData.swift +++ b/submodules/TelegramUI/Sources/ChatControllerContentData.swift @@ -811,7 +811,6 @@ extension ChatControllerImpl { var sendPaidMessageStars: StarsAmount? var alwaysShowGiftButton = false var disallowedGifts: TelegramDisallowedGifts? - var switchToBotForum: EnginePeer.Id? if let peer = peerView.peers[peerView.peerId] { if let cachedData = peerView.cachedData as? CachedUserData { contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot) @@ -826,11 +825,6 @@ extension ChatControllerImpl { } disallowedGifts = cachedData.disallowedGifts } - if case let .known(value) = cachedData.linkedBotChannelId { - if let value { - switchToBotForum = value - } - } } else if let cachedData = peerView.cachedData as? CachedGroupData { var invitedBy: Peer? if let invitedByPeerId = cachedData.invitedBy { @@ -1030,15 +1024,6 @@ extension ChatControllerImpl { strongSelf.state.renderedPeer = renderedPeer strongSelf.state.adMessage = adMessage - - if let switchToBotForum { - strongSelf.state.historyNodeData = HistoryNodeData( - chatLocation: .peer(id: switchToBotForum), - chatLocationContextHolder: Atomic(value: nil) - ) - } else { - strongSelf.state.historyNodeData = nil - } if case .standard(.default) = mode, let channel = renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info { var isRegularChat = false @@ -1459,12 +1444,19 @@ extension ChatControllerImpl { ) var customMessageCount: Int? - if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.isMonoForum { + var customSubtitle: String? + if let peer = peerView.peers[peerView.peerId] as? TelegramChannel { + if peer.isMonoForum { + } else if peer.linkedBotId != nil { + customSubtitle = strongSelf.presentationData.strings.Bot_GenericBotStatus + } else { + customMessageCount = savedMessagesPeer?.messageCount ?? 0 + } } else { customMessageCount = savedMessagesPeer?.messageCount ?? 0 } - strongSelf.state.chatTitleContent = .peer(peerView: mappedPeerData, customTitle: nil, customSubtitle: nil, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: false, customMessageCount: customMessageCount, isEnabled: true) + strongSelf.state.chatTitleContent = .peer(peerView: mappedPeerData, customTitle: nil, customSubtitle: customSubtitle, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: false, customMessageCount: customMessageCount, isEnabled: true) strongSelf.state.peerView = peerView @@ -1551,7 +1543,15 @@ extension ChatControllerImpl { } if let threadInfo = messageAndTopic.threadData?.info { - strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: threadInfo.title, customSubtitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: peerIsMuted, customMessageCount: messageAndTopic.messageCount == 0 ? nil : messageAndTopic.messageCount, isEnabled: true) + var customSubtitle: String? + if messageAndTopic.messageCount == 0, let peer = peerView.peers[peerView.peerId] as? TelegramChannel { + if peer.linkedBotId != nil { + //TODO:localize + customSubtitle = "topic" + } + } + + strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: threadInfo.title, customSubtitle: customSubtitle, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: peerIsMuted, customMessageCount: messageAndTopic.messageCount == 0 ? nil : messageAndTopic.messageCount, isEnabled: true) let avatarContent: EmojiStatusComponent.Content if chatLocation.threadId == 1 { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index 3aba8f546d..eb06530a15 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -132,6 +132,13 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present } } + if let user = presentationInterfaceState.renderedPeer?.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), let moreInfoNavigationButton = moreInfoNavigationButton { + if case .pinnedMessages = presentationInterfaceState.subject { + } else { + return moreInfoNavigationButton + } + } + if case .messageOptions = presentationInterfaceState.subject { return nil } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index af06d64b1e..c0baff10b0 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -237,7 +237,7 @@ func titleTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterfa if let currentPanel = currentPanel as? ChatTopicListTitleAccessoryPanelNode { return currentPanel } else { - let panel = ChatTopicListTitleAccessoryPanelNode(context: context, peerId: peerId, isMonoforum: true) + let panel = ChatTopicListTitleAccessoryPanelNode(context: context, peerId: peerId, kind: .monoforum) panel.interfaceInteraction = interfaceInteraction return panel } @@ -248,7 +248,18 @@ func titleTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterfa if let currentPanel = currentPanel as? ChatTopicListTitleAccessoryPanelNode { return currentPanel } else { - let panel = ChatTopicListTitleAccessoryPanelNode(context: context, peerId: peerId, isMonoforum: false) + let panel = ChatTopicListTitleAccessoryPanelNode(context: context, peerId: peerId, kind: channel.linkedBotId != nil ? .botForum : .forum) + panel.interfaceInteraction = interfaceInteraction + return panel + } + } + } else if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), chatPresentationInterfaceState.search == nil { + let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation + if !topicListDisplayModeOnTheSide, let peerId = chatPresentationInterfaceState.chatLocation.peerId { + if let currentPanel = currentPanel as? ChatTopicListTitleAccessoryPanelNode { + return currentPanel + } else { + let panel = ChatTopicListTitleAccessoryPanelNode(context: context, peerId: peerId, kind: .botForum) panel.interfaceInteraction = interfaceInteraction return panel } @@ -279,7 +290,7 @@ func sidePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState strings: chatPresentationInterfaceState.strings, location: .side, peerId: peerId, - isMonoforum: true, + kind: .monoforum, topicId: chatPresentationInterfaceState.chatLocation.threadId, controller: { [weak interfaceInteraction] in return interfaceInteraction?.chatController() @@ -310,7 +321,38 @@ func sidePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState strings: chatPresentationInterfaceState.strings, location: .side, peerId: peerId, - isMonoforum: false, + kind: channel.linkedBotId != nil ? .botForum : .forum, + topicId: chatPresentationInterfaceState.chatLocation.threadId, + controller: { [weak interfaceInteraction] in + return interfaceInteraction?.chatController() + }, + togglePanel: { [weak interfaceInteraction] in + interfaceInteraction?.toggleChatSidebarMode() + }, + updateTopicId: { [weak interfaceInteraction] topicId, direction in + interfaceInteraction?.updateChatLocationThread(topicId, direction ? .down : .up) + }, + openDeletePeer: { [weak interfaceInteraction] threadId in + guard let controller = interfaceInteraction?.chatController() as? ChatControllerImpl else { + return + } + controller.openDeleteMonoforumPeer(peerId: EnginePeer.Id(threadId)) + } + )) + ) + } + } else if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), chatPresentationInterfaceState.search == nil { + let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation + if topicListDisplayModeOnTheSide { + return AnyComponentWithIdentity( + id: "topics", + component: AnyComponent(ChatSideTopicsPanel( + context: context, + theme: chatPresentationInterfaceState.theme, + strings: chatPresentationInterfaceState.strings, + location: .side, + peerId: peerId, + kind: .botForum, topicId: chatPresentationInterfaceState.chatLocation.threadId, controller: { [weak interfaceInteraction] in return interfaceInteraction?.chatController() diff --git a/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift index b913f1078c..0da1b30f5b 100644 --- a/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift @@ -53,17 +53,15 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C private let context: AccountContext private let peerId: EnginePeer.Id - private let isMonoforum: Bool + private let kind: ChatSideTopicsPanel.Kind private let panel = ComponentView() - init(context: AccountContext, peerId: EnginePeer.Id, isMonoforum: Bool) { + init(context: AccountContext, peerId: EnginePeer.Id, kind: ChatSideTopicsPanel.Kind) { self.context = context self.peerId = peerId - self.isMonoforum = isMonoforum + self.kind = kind super.init() - - } deinit { @@ -103,7 +101,7 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C strings: params.interfaceState.strings, location: .top, peerId: self.peerId, - isMonoforum: self.isMonoforum, + kind: self.kind, topicId: params.interfaceState.chatLocation.threadId, controller: { [weak self] in return self?.interfaceInteraction?.chatController()