This commit is contained in:
Isaac 2025-08-15 09:18:26 +02:00
parent 8843fbe5cb
commit b4a0e8809c
21 changed files with 746 additions and 301 deletions

View file

@ -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<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(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<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(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

View file

@ -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
}
}
}

View file

@ -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))
}
})
})))
}
}

View file

@ -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<EnginePeer?, NoError> = .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<EnginePeer?, NoError> = .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

View file

@ -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 {

View file

@ -373,7 +373,7 @@ final class ChatListTable: Table {
return result
}
func replay(historyOperationsByPeerId: [PeerId: [MessageHistoryOperation]], updatedPeerChatListEmbeddedStates: Set<PeerId>, updatedChatListInclusions: [PeerId: PeerChatListInclusion], messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, operations: inout [PeerGroupId: [ChatListOperation]]) {
func replay(postbox: PostboxImpl, historyOperationsByPeerId: [PeerId: [MessageHistoryOperation]], updatedPeerChatListEmbeddedStates: Set<PeerId>, updatedPeers: [PeerId: Peer], updatedChatListInclusions: [PeerId: PeerChatListInclusion], messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, operations: inout [PeerGroupId: [ChatListOperation]]) {
var changedPeerIds = Set<PeerId>()
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)

View file

@ -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,

View file

@ -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<GlobalMessageIdsNamespace>,
@ -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
}
}

View file

@ -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
}
)
}()

View file

@ -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<Empty>?
public var storyData: (hasUnseen: Bool, hasUnseenCloseFriends: Bool)?
public let statusView: ComponentView<Empty>
public var statusView: ComponentView<Empty>
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 {

View file

@ -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(

View file

@ -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<EngineChatList, NoError> = 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<EnginePeer.Id?, NoError>
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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)
}
}

View file

@ -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<ChatLocationContextHolder?>(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<ChatLocationContextHolder?>(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 {

View file

@ -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 {

View file

@ -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
}

View file

@ -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()

View file

@ -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<ChatSidePanelEnvironment>()
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()