diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 55958e98fd..0fc1d1bdc3 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -507,6 +507,157 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { private let maxVideoLoopCount = 3 class ChatListItemNode: ItemListRevealOptionsItemNode { + final class AuthorNode: ASDisplayNode { + let authorNode: TextNode + var titleTopicArrowNode: ASImageNode? + var topicTitleNode: TextNode? + var titleTopicIconView: ComponentHostView? + var titleTopicIconComponent: EmojiStatusComponent? + + var visibilityStatus: Bool = false { + didSet { + if self.visibilityStatus != oldValue { + if let titleTopicIconView = self.titleTopicIconView, let titleTopicIconComponent = self.titleTopicIconComponent { + let _ = titleTopicIconView.update( + transition: .immediate, + component: AnyComponent(titleTopicIconComponent.withVisibleForAnimations(self.visibilityStatus)), + environment: {}, + containerSize: titleTopicIconView.bounds.size + ) + } + } + } + } + + override init() { + self.authorNode = TextNode() + self.authorNode.displaysAsynchronously = true + + super.init() + + self.addSubnode(self.authorNode) + } + + func asyncLayout() -> (_ context: AccountContext, _ constrainedWidth: CGFloat, _ theme: PresentationTheme, _ authorTitle: NSAttributedString?, _ topic: (title: NSAttributedString, iconId: Int64?, iconColor: Int32)?) -> (CGSize, () -> Void) { + let makeAuthorLayout = TextNode.asyncLayout(self.authorNode) + let makeTopicTitleLayout = TextNode.asyncLayout(self.topicTitleNode) + + return { [weak self] context, constrainedWidth, theme, authorTitle, topic in + var maxTitleWidth = constrainedWidth + if let _ = topic { + maxTitleWidth = floor(constrainedWidth * 0.7) + } + + let authorTitleLayout = makeAuthorLayout(TextNodeLayoutArguments(attributedString: authorTitle, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + + var remainingWidth = constrainedWidth - authorTitleLayout.0.size.width + + var topicTitleArguments: TextNodeLayoutArguments? + var arrowIconImage: UIImage? + if let topic = topic { + remainingWidth -= 22.0 + 2.0 + + arrowIconImage = PresentationResourcesChatList.topicArrowIcon(theme) + if let arrowIconImage = arrowIconImage { + remainingWidth -= arrowIconImage.size.width + 6.0 * 2.0 + } + + topicTitleArguments = TextNodeLayoutArguments(attributedString: topic.title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: remainingWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)) + } + + let topicTitleLayout = topicTitleArguments.flatMap(makeTopicTitleLayout) + + var size = authorTitleLayout.0.size + if let topicTitleLayout = topicTitleLayout { + size.width += 10.0 + topicTitleLayout.0.size.width + } + + return (size, { + guard let self else { + return + } + + let _ = authorTitleLayout.1() + let authorFrame = CGRect(origin: CGPoint(), size: authorTitleLayout.0.size) + self.authorNode.frame = authorFrame + + var nextX = authorFrame.maxX - 1.0 + if let arrowIconImage = arrowIconImage, let topic = topic { + let titleTopicArrowNode: ASImageNode + if let current = self.titleTopicArrowNode { + titleTopicArrowNode = current + } else { + titleTopicArrowNode = ASImageNode() + self.titleTopicArrowNode = titleTopicArrowNode + self.addSubnode(titleTopicArrowNode) + } + titleTopicArrowNode.image = arrowIconImage + nextX += 6.0 + titleTopicArrowNode.frame = CGRect(origin: CGPoint(x: nextX, y: 5.0), size: arrowIconImage.size) + nextX += arrowIconImage.size.width + 6.0 + + let titleTopicIconView: ComponentHostView + if let current = self.titleTopicIconView { + titleTopicIconView = current + } else { + titleTopicIconView = ComponentHostView() + self.titleTopicIconView = titleTopicIconView + self.view.addSubview(titleTopicIconView) + } + + let titleTopicIconContent: EmojiStatusComponent.Content + if let fileId = topic.iconId, fileId != 0 { + titleTopicIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 36.0, height: 36.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(2)) + } else { + titleTopicIconContent = .topic(title: String(topic.title.string.prefix(1)), color: topic.iconColor, size: CGSize(width: 22.0, height: 22.0)) + } + + let titleTopicIconComponent = EmojiStatusComponent( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + content: titleTopicIconContent, + isVisibleForAnimations: self.visibilityStatus, + action: nil + ) + self.titleTopicIconComponent = titleTopicIconComponent + + let iconSize = titleTopicIconView.update( + transition: .immediate, + component: AnyComponent(titleTopicIconComponent), + environment: {}, + containerSize: CGSize(width: 22.0, height: 22.0) + ) + titleTopicIconView.frame = CGRect(origin: CGPoint(x: nextX, y: UIScreenPixel), size: iconSize) + nextX += iconSize.width + 2.0 + } else { + if let titleTopicArrowNode = self.titleTopicArrowNode { + self.titleTopicArrowNode = nil + titleTopicArrowNode.removeFromSupernode() + } + if let titleTopicIconView = self.titleTopicIconView { + self.titleTopicIconView = nil + titleTopicIconView.removeFromSuperview() + } + } + + if let topicTitleLayout = topicTitleLayout { + let topicTitleNode = topicTitleLayout.1() + if topicTitleNode.supernode == nil { + self.addSubnode(topicTitleNode) + self.topicTitleNode = topicTitleNode + } + + topicTitleNode.frame = CGRect(origin: CGPoint(x: nextX - 1.0, y: 0.0), size: topicTitleLayout.0.size) + } else if let topicTitleNode = self.topicTitleNode { + self.topicTitleNode = nil + topicTitleNode.removeFromSupernode() + } + }) + } + } + } + var item: ChatListItem? private let backgroundNode: ASDisplayNode @@ -523,7 +674,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private var videoLoopCount = 0 let titleNode: TextNode - let authorNode: TextNode + let authorNode: AuthorNode let measureNode: TextNode private var currentItemHeight: CGFloat? let textNode: TextNodeWithEntities @@ -731,6 +882,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { containerSize: avatarIconView.bounds.size ) } + self.authorNode.visibilityStatus = self.visibilityStatus } } } @@ -766,9 +918,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = true - self.authorNode = TextNode() + self.authorNode = AuthorNode() self.authorNode.isUserInteractionEnabled = false - self.authorNode.displaysAsynchronously = true self.textNode = TextNodeWithEntities() self.textNode.textNode.isUserInteractionEnabled = false @@ -1045,7 +1196,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let dateLayout = TextNode.asyncLayout(self.dateNode) let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) let titleLayout = TextNode.asyncLayout(self.titleNode) - let authorLayout = TextNode.asyncLayout(self.authorNode) + let authorLayout = self.authorNode.asyncLayout() let makeMeasureLayout = TextNode.asyncLayout(self.measureNode) let inputActivitiesLayout = self.inputActivitiesNode.asyncLayout() let badgeLayout = self.badgeNode.asyncLayout() @@ -1286,6 +1437,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let contentImageSpacing: CGFloat = 2.0 let contentImageTrailingSpace: CGFloat = 5.0 var contentImageSpecs: [(message: EngineMessage, media: EngineMedia, size: CGSize)] = [] + var forumThread: (title: String, iconId: Int64?, iconColor: Int32)? switch contentData { case let .chat(itemPeer, _, _, _, text, spoilers, customEmojiRanges): @@ -1310,14 +1462,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - if let peerTextValue = peerText, case let .channel(channel) = itemPeer.chatMainPeer, channel.flags.contains(.isForum), threadInfo == nil { + if let _ = peerText, case let .channel(channel) = itemPeer.chatMainPeer, channel.flags.contains(.isForum), threadInfo == nil { if let forumTopicData = forumTopicData { - peerText = "\(peerTextValue) → \(forumTopicData.title)" + forumThread = (forumTopicData.title, forumTopicData.iconFileId, forumTopicData.iconColor) } else if let threadInfo = threadInfo?.info { - peerText = "\(peerTextValue) → \(threadInfo.title)" - } else { - //TODO:localize - peerText = "\(peerTextValue) → General" + forumThread = (threadInfo.title, threadInfo.icon, threadInfo.iconColor) } } @@ -1582,14 +1731,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if hasFailedMessages { statusState = .failed(item.presentationData.theme.chatList.failedFillColor, item.presentationData.theme.chatList.failedForegroundColor) } else { - if case .chatList = item.chatListLocation { - if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) { + if let forumTopicData = forumTopicData { + if message.id.namespace == forumTopicData.maxOutgoingReadMessageId.namespace, message.id.id >= forumTopicData.maxOutgoingReadMessageId.id { statusState = .read(item.presentationData.theme.chatList.checkmarkColor) } else { statusState = .delivered(item.presentationData.theme.chatList.checkmarkColor) } - } else if case .forum = item.chatListLocation { - if let forumTopicData = forumTopicData, message.id.namespace == forumTopicData.maxOutgoingReadMessageId.namespace, message.id.id >= forumTopicData.maxOutgoingReadMessageId.id { + } else { + if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) { statusState = .read(item.presentationData.theme.chatList.checkmarkColor) } else { statusState = .delivered(item.presentationData.theme.chatList.checkmarkColor) @@ -1776,7 +1925,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } badgeSize = max(badgeSize, reorderInset) - let (authorLayout, authorApply) = authorLayout(TextNodeLayoutArguments(attributedString: (hideAuthor && !hasDraft) ? nil : authorAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + let authorTitle = (hideAuthor && !hasDraft) ? nil : authorAttributedString + + var forumThreadTitle: (title: NSAttributedString, iconId: Int64?, iconColor: Int32)? + if let _ = authorTitle, let forumThread { + forumThreadTitle = (NSAttributedString(string: forumThread.title, font: textFont, textColor: theme.authorNameColor), forumThread.iconId, forumThread.iconColor) + } + + let (authorLayout, authorApply) = authorLayout(item.context, rawContentWidth - badgeSize, item.presentationData.theme, authorTitle, forumThreadTitle) var textCutout: TextNodeCutout? if !textLeftCutout.isZero { @@ -1869,7 +2025,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let threadInfo { isClosed = threadInfo.isClosed } - peerRevealOptions = forumRevealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isMuted: (currentMutedIconImage != nil), isClosed: isClosed, isPinned: isPinned, isEditing: item.editing, canPin: channel.hasPermission(.pinMessages), canManage: canManage) + peerRevealOptions = forumRevealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isMuted: (currentMutedIconImage != nil), isClosed: isClosed, isPinned: isPinned, isEditing: item.editing, canPin: channel.flags.contains(.isCreator) || channel.adminRights != nil, canManage: canManage) peerLeftRevealOptions = [] } else { peerRevealOptions = [] @@ -2205,13 +2361,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.secretIconNode = nil secretIconNode.removeFromSupernode() } - + let contentDelta = CGPoint(x: contentRect.origin.x - (strongSelf.titleNode.frame.minX - titleOffset), y: contentRect.origin.y - (strongSelf.titleNode.frame.minY - UIScreenPixel)) let titleFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame - let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height), size: authorLayout.size) + let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height), size: authorLayout) strongSelf.authorNode.frame = authorNodeFrame - let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.size.height.isZero ? 0.0 : (authorLayout.size.height - 3.0))), size: textLayout.size) + let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.height.isZero ? 0.0 : (authorLayout.height - 3.0))), size: textLayout.size) strongSelf.textNode.textNode.frame = textNodeFrame if !textLayout.spoilers.isEmpty { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift index a0181137f6..e7a1b197cf 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift @@ -232,7 +232,7 @@ func chatListViewForLocation(chatListLocation: ChatListControllerLocation, locat pinnedIndex = .none } - let readCounters = EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: 1, maxOutgoingReadId: 1, maxKnownId: 1, count: data.incomingUnreadCount, markedUnread: false))]), isMuted: false) + let readCounters = EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: 1, maxOutgoingReadId: data.maxOutgoingReadId, maxKnownId: 1, count: data.incomingUnreadCount, markedUnread: false))]), isMuted: false) var draft: EngineChatList.Draft? if let embeddedState = item.embeddedInterfaceState, let _ = embeddedState.overrideChatTimestamp { diff --git a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift index 97be3219c5..e50289ef00 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift @@ -204,6 +204,11 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV fromEmptyView = true } + if let fromView = fromView, !fromView.isLoading, toView.isLoading { + options.remove(.AnimateInsertion) + options.remove(.AnimateAlpha) + } + var adjustScrollToFirstItem = false if !previewing && !searchMode && fromEmptyView && scrollToItem == nil && toView.filteredEntries.count >= 2 { adjustScrollToFirstItem = true diff --git a/submodules/Postbox/Sources/ChatListHolesView.swift b/submodules/Postbox/Sources/ChatListHolesView.swift index c1c1d103db..89bbc5d4a5 100644 --- a/submodules/Postbox/Sources/ChatListHolesView.swift +++ b/submodules/Postbox/Sources/ChatListHolesView.swift @@ -30,3 +30,35 @@ public final class ChatListHolesView { self.entries = mutableView.entries } } + +public struct ForumTopicListHolesEntry: Hashable { + public let peerId: PeerId + public let index: StoredPeerThreadCombinedState.Index? + + public init(peerId: PeerId, index: StoredPeerThreadCombinedState.Index?) { + self.peerId = peerId + self.index = index + } +} + +final class MutableForumTopicListHolesView { + fileprivate var entries = Set() + + func update(holes: Set) -> Bool { + if self.entries != holes { + self.entries = holes + return true + } else { + return false + } + } +} + +public final class ForumTopicListHolesView { + public let entries: Set + + init(_ mutableView: MutableForumTopicListHolesView) { + self.entries = mutableView.entries + } +} + diff --git a/submodules/Postbox/Sources/MessageHistoryThreadIndexView.swift b/submodules/Postbox/Sources/MessageHistoryThreadIndexView.swift index b685eaa561..8d7f30da7a 100644 --- a/submodules/Postbox/Sources/MessageHistoryThreadIndexView.swift +++ b/submodules/Postbox/Sources/MessageHistoryThreadIndexView.swift @@ -33,6 +33,7 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView { fileprivate let summaryComponents: ChatListEntrySummaryComponents fileprivate var peer: Peer? fileprivate var items: [Item] = [] + private var hole: ForumTopicListHolesEntry? fileprivate var isLoading: Bool = false init(postbox: PostboxImpl, peerId: PeerId, summaryComponents: ChatListEntrySummaryComponents) { @@ -50,6 +51,16 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView { let validIndexBoundary = postbox.peerThreadCombinedStateTable.get(peerId: peerId)?.validIndexBoundary self.isLoading = validIndexBoundary == nil + if let validIndexBoundary = validIndexBoundary { + if validIndexBoundary.messageId != 1 { + self.hole = ForumTopicListHolesEntry(peerId: self.peerId, index: validIndexBoundary) + } else { + self.hole = nil + } + } else { + self.hole = ForumTopicListHolesEntry(peerId: self.peerId, index: nil) + } + if !self.isLoading { let pinnedThreadIds = postbox.messageHistoryThreadPinnedTable.get(peerId: self.peerId) var nextPinnedIndex = 0 @@ -124,9 +135,15 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView { return updated } + + func topHole() -> ForumTopicListHolesEntry? { + return self.hole + } func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool { - return false + self.reload(postbox: postbox) + + return true } func immutableView() -> PostboxView { diff --git a/submodules/Postbox/Sources/PeerThreadCombinedStateTable.swift b/submodules/Postbox/Sources/PeerThreadCombinedStateTable.swift index 81a3b1739d..2109cf602d 100644 --- a/submodules/Postbox/Sources/PeerThreadCombinedStateTable.swift +++ b/submodules/Postbox/Sources/PeerThreadCombinedStateTable.swift @@ -1,7 +1,7 @@ import Foundation public struct StoredPeerThreadCombinedState: Equatable, Codable { - public struct Index: Equatable, Comparable, Codable { + public struct Index: Hashable, Comparable, Codable { private enum CodingKeys: String, CodingKey { case timestamp = "t" case threadId = "i" diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index ca5add8f93..615e476ff1 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -3281,6 +3281,18 @@ final class PostboxImpl { } } + public func forumTopicListHolesView() -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.queue.async { + disposable.set(self.viewTracker.forumTopicListHolesViewSignal().start(next: { view in + subscriber.putNext(view) + })) + } + return disposable + } + } + public func unsentMessageIdsView() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() @@ -4195,6 +4207,18 @@ public class Postbox { return disposable } } + + public func forumTopicListHolesView() -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + self.impl.with { impl in + disposable.set(impl.forumTopicListHolesView().start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)) + } + + return disposable + } + } public func unsentMessageIdsView() -> Signal { return Signal { subscriber in diff --git a/submodules/Postbox/Sources/PostboxView.swift b/submodules/Postbox/Sources/PostboxView.swift index 710fb0b21a..7cf89152ae 100644 --- a/submodules/Postbox/Sources/PostboxView.swift +++ b/submodules/Postbox/Sources/PostboxView.swift @@ -10,7 +10,7 @@ protocol MutablePostboxView { } final class CombinedMutableView { - private let views: [PostboxViewKey: MutablePostboxView] + let views: [PostboxViewKey: MutablePostboxView] init(views: [PostboxViewKey: MutablePostboxView]) { self.views = views diff --git a/submodules/Postbox/Sources/ViewTracker.swift b/submodules/Postbox/Sources/ViewTracker.swift index fa275fc344..e544bb82fe 100644 --- a/submodules/Postbox/Sources/ViewTracker.swift +++ b/submodules/Postbox/Sources/ViewTracker.swift @@ -25,6 +25,9 @@ final class ViewTracker { private let chatListHolesView = MutableChatListHolesView() private let chatListHolesViewSubscribers = Bag>() + private let forumTopicListHolesView = MutableForumTopicListHolesView() + private let forumTopicListHolesViewSubscribers = Bag>() + private var unsentMessageView: UnsentMessageHistoryView private let unsendMessageIdsViewSubscribers = Bag>() @@ -407,6 +410,8 @@ final class ViewTracker { pipe.putNext(view.immutableView()) } } + + self.updateTrackedForumTopicListHoles() } private func updateTrackedChatListHoles() { @@ -425,6 +430,26 @@ final class ViewTracker { } } + private func updateTrackedForumTopicListHoles() { + var firstHoles = Set() + + for (views) in self.combinedViews.copyItems() { + for (key, view) in views.0.views { + if case .messageHistoryThreadIndex = key, let view = view as? MutableMessageHistoryThreadIndexView { + if let hole = view.topHole() { + firstHoles.insert(hole) + } + } + } + } + + if self.forumTopicListHolesView.update(holes: firstHoles) { + for pipe in self.forumTopicListHolesViewSubscribers.copyItems() { + pipe.putNext(ForumTopicListHolesView(self.forumTopicListHolesView)) + } + } + } + private func updateTrackedHoles() { var firstHolesAndTags = Set() for (view, _) in self.messageHistoryViews.copyItems() { @@ -506,6 +531,30 @@ final class ViewTracker { } } + func forumTopicListHolesViewSignal() -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.queue.async { + subscriber.putNext(ForumTopicListHolesView(self.forumTopicListHolesView)) + + let pipe = ValuePipe() + let index = self.forumTopicListHolesViewSubscribers.add(pipe) + + let pipeDisposable = pipe.signal().start(next: { view in + subscriber.putNext(view) + }) + + disposable.set(ActionDisposable { + self.queue.async { + pipeDisposable.dispose() + self.forumTopicListHolesViewSubscribers.remove(index) + } + }) + } + return disposable + } + } + func unsentMessageIdsViewSignal() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() diff --git a/submodules/TelegramCore/Sources/ForumChannels.swift b/submodules/TelegramCore/Sources/ForumChannels.swift index 30099e0982..a0c2d8ce23 100644 --- a/submodules/TelegramCore/Sources/ForumChannels.swift +++ b/submodules/TelegramCore/Sources/ForumChannels.swift @@ -373,22 +373,6 @@ func _internal_setForumChannelTopicPinned(account: Account, id: EnginePeer.Id, t account.stateManager.addUpdates(result) return .complete() - - /*return account.postbox.transaction { transaction -> Void in - if let initialData = transaction.getMessageHistoryThreadInfo(peerId: id, threadId: threadId)?.data.get(MessageHistoryThreadData.self) { - var data = initialData - - data.isClosed = isClosed - - if data != initialData { - if let entry = StoredMessageHistoryThreadInfo(data) { - transaction.setMessageHistoryThreadInfo(peerId: id, threadId: threadId, info: entry) - } - } - } - } - |> castError(EditForumChannelTopicError.self) - |> ignoreValues*/ } } } @@ -421,8 +405,8 @@ enum LoadMessageHistoryThreadsError { case generic } -func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Signal { - let signal: Signal = account.postbox.transaction { transaction -> Api.InputChannel? in +func _internal_loadMessageHistoryThreads(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: PeerId, offsetIndex: StoredPeerThreadCombinedState.Index?, limit: Int) -> Signal { + let signal: Signal = postbox.transaction { transaction -> Api.InputChannel? in return transaction.getPeer(peerId).flatMap(apiInputChannel) } |> castError(LoadMessageHistoryThreadsError.self) @@ -430,23 +414,32 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si guard let inputChannel = inputChannel else { return .fail(.generic) } - let signal: Signal = account.network.request(Api.functions.channels.getForumTopics( - flags: 0, + let flags: Int32 = 0 + var offsetDate: Int32 = 0 + var offsetId: Int32 = 0 + var offsetTopic: Int32 = 0 + if let offsetIndex = offsetIndex { + offsetDate = offsetIndex.timestamp + offsetId = offsetIndex.messageId + offsetTopic = Int32(clamping: offsetIndex.threadId) + } + let signal: Signal = network.request(Api.functions.channels.getForumTopics( + flags: flags, channel: inputChannel, q: nil, - offsetDate: 0, - offsetId: 0, - offsetTopic: 0, - limit: 100 + offsetDate: offsetDate, + offsetId: offsetId, + offsetTopic: offsetTopic, + limit: Int32(limit) )) |> mapError { _ -> LoadMessageHistoryThreadsError in return .generic } |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction -> Void in + return postbox.transaction { transaction -> Void in var pinnedId: Int64? switch result { - case let .forumTopics(flags, count, topics, messages, chats, users, pts): + case let .forumTopics(_, _, topics, messages, chats, users, pts): var peers: [Peer] = [] var peerPresences: [PeerId: Api.User] = [:] for chat in chats { @@ -463,19 +456,16 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si return updated }) - updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) + updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) - let _ = InternalAccountState.addMessages(transaction: transaction, messages: messages.compactMap { message -> StoreMessage? in + let addedMessages = messages.compactMap { message -> StoreMessage? in return StoreMessage(apiMessage: message) - }, location: .Random) + } + + let _ = InternalAccountState.addMessages(transaction: transaction, messages: addedMessages, location: .Random) - let _ = flags - let _ = count - let _ = topics - let _ = messages - let _ = chats - let _ = users let _ = pts + var minIndex: StoredPeerThreadCombinedState.Index? for topic in topics { switch topic { @@ -509,6 +499,22 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si transaction.replaceMessageTagSummary(peerId: peerId, threadId: Int64(id), tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, count: unreadMentionsCount, maxId: topMessage) transaction.replaceMessageTagSummary(peerId: peerId, threadId: Int64(id), tagMask: .unseenReaction, namespace: Namespaces.Message.Cloud, count: unreadReactionsCount, maxId: topMessage) + + var topTimestamp = date + for message in addedMessages { + if message.id.peerId == peerId && message.threadId == Int64(id) { + topTimestamp = max(topTimestamp, message.timestamp) + } + } + + let topicIndex = StoredPeerThreadCombinedState.Index(timestamp: topTimestamp, threadId: Int64(id), messageId: topMessage) + if let minIndexValue = minIndex { + if topicIndex < minIndexValue { + minIndex = topicIndex + } + } else { + minIndex = topicIndex + } case .forumTopicDeleted: break } @@ -520,9 +526,17 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si transaction.setPeerPinnedThreads(peerId: peerId, threadIds: []) } - if let entry = StoredPeerThreadCombinedState(PeerThreadCombinedState( - validIndexBoundary: StoredPeerThreadCombinedState.Index(timestamp: Int32.max, threadId: Int64(Int32.max), messageId: Int32.max) - )) { + var nextIndex: StoredPeerThreadCombinedState.Index + if topics.count != 0 { + nextIndex = minIndex ?? StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1) + } else { + nextIndex = StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1) + } + if let offsetIndex = offsetIndex, nextIndex == offsetIndex { + nextIndex = StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1) + } + + if let entry = StoredPeerThreadCombinedState(PeerThreadCombinedState(validIndexBoundary: nextIndex)) { transaction.setPeerThreadCombinedState(peerId: peerId, state: entry) } } @@ -641,7 +655,7 @@ public final class ForumChannelTopics { self.account = account self.peerId = peerId - let _ = _internal_loadMessageHistoryThreads(account: self.account, peerId: peerId).start() + //let _ = _internal_loadMessageHistoryThreads(account: self.account, peerId: peerId, offsetIndex: nil, limit: 100).start() self.updateDisposable.set(account.viewTracker.polledChannel(peerId: peerId).start()) } diff --git a/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift b/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift index 920f6040ac..d452e38546 100644 --- a/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift +++ b/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift @@ -90,3 +90,70 @@ func managedChatListHoles(network: Network, postbox: Postbox, accountPeerId: Pee } } } + +private final class ManagedForumTopicListHolesState { + private var currentHoles: [ForumTopicListHolesEntry: Disposable] = [:] + + func clearDisposables() -> [Disposable] { + let disposables = Array(self.currentHoles.values) + self.currentHoles.removeAll() + return disposables + } + + func update(entries: [ForumTopicListHolesEntry]) -> (removed: [Disposable], added: [ForumTopicListHolesEntry: MetaDisposable]) { + var removed: [Disposable] = [] + var added: [ForumTopicListHolesEntry: MetaDisposable] = [:] + + for entry in entries { + if self.currentHoles[entry] == nil { + let disposable = MetaDisposable() + added[entry] = disposable + self.currentHoles[entry] = disposable + } + } + + var removedKeys: [ForumTopicListHolesEntry] = [] + for (entry, disposable) in self.currentHoles { + if !entries.contains(entry) { + removed.append(disposable) + removedKeys.append(entry) + } + } + for key in removedKeys { + self.currentHoles.removeValue(forKey: key) + } + + return (removed, added) + } +} + +func managedForumTopicListHoles(network: Network, postbox: Postbox, accountPeerId: PeerId) -> Signal { + return Signal { _ in + let state = Atomic(value: ManagedForumTopicListHolesState()) + + let disposable = postbox.forumTopicListHolesView().start(next: { view in + let entries = Array(view.entries) + + let (removed, added) = state.with { state in + return state.update(entries: entries) + } + + for disposable in removed { + disposable.dispose() + } + + for (entry, disposable) in added { + disposable.set(_internal_loadMessageHistoryThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, peerId: entry.peerId, offsetIndex: entry.index, limit: 100).start()) + } + }) + + return ActionDisposable { + disposable.dispose() + for disposable in state.with({ state -> [Disposable] in + state.clearDisposables() + }) { + disposable.dispose() + } + } + } +} diff --git a/submodules/TelegramCore/Sources/State/ManagedServiceViews.swift b/submodules/TelegramCore/Sources/State/ManagedServiceViews.swift index 03eaf49c90..1286e780d4 100644 --- a/submodules/TelegramCore/Sources/State/ManagedServiceViews.swift +++ b/submodules/TelegramCore/Sources/State/ManagedServiceViews.swift @@ -7,6 +7,7 @@ func managedServiceViews(accountPeerId: PeerId, network: Network, postbox: Postb let disposable = DisposableSet() disposable.add(managedMessageHistoryHoles(accountPeerId: accountPeerId, network: network, postbox: postbox).start()) disposable.add(managedChatListHoles(network: network, postbox: postbox, accountPeerId: accountPeerId).start()) + disposable.add(managedForumTopicListHoles(network: network, postbox: postbox, accountPeerId: accountPeerId).start()) disposable.add(managedSynchronizePeerReadStates(network: network, postbox: postbox, stateManager: stateManager).start()) disposable.add(managedSynchronizeGroupMessageStats(network: network, postbox: postbox, stateManager: stateManager).start()) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift index 646dd30a09..38e66f82e8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift @@ -29,10 +29,14 @@ public final class EngineChatList: Equatable { public struct ForumTopicData: Equatable { public var title: String + public let iconFileId: Int64? + public let iconColor: Int32 public var maxOutgoingReadMessageId: EngineMessage.Id - public init(title: String, maxOutgoingReadMessageId: EngineMessage.Id) { + public init(title: String, iconFileId: Int64?, iconColor: Int32, maxOutgoingReadMessageId: EngineMessage.Id) { self.title = title + self.iconFileId = iconFileId + self.iconColor = iconColor self.maxOutgoingReadMessageId = maxOutgoingReadMessageId } } @@ -422,7 +426,7 @@ extension EngineChatList.Item { var forumTopicDataValue: EngineChatList.ForumTopicData? if let forumTopicData = forumTopicData?.data.get(MessageHistoryThreadData.self) { - forumTopicDataValue = EngineChatList.ForumTopicData(title: forumTopicData.info.title, maxOutgoingReadMessageId: MessageId(peerId: index.messageIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: forumTopicData.maxOutgoingReadId)) + forumTopicDataValue = EngineChatList.ForumTopicData(title: forumTopicData.info.title, iconFileId: forumTopicData.info.icon, iconColor: forumTopicData.info.iconColor, maxOutgoingReadMessageId: MessageId(peerId: index.messageIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: forumTopicData.maxOutgoingReadId)) } let readCounters = readState.flatMap(EnginePeerReadCounters.init) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index a0bf7e87ec..8c7c01f4e4 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -95,6 +95,7 @@ public enum PresentationResourceKey: Int32 { case chatListFakeServiceIcon case chatListSecretIcon case chatListStatusLockIcon + case chatListTopicArrowIcon case chatListRecentStatusOnlineIcon case chatListRecentStatusOnlineHighlightedIcon case chatListRecentStatusOnlinePinnedIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift index e1e42135a3..6f5bf37308 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift @@ -360,4 +360,10 @@ public struct PresentationResourcesChatList { return generateTintedImage(image: UIImage(bundleImageName: "Chat List/StatusLockIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor) }) } + + public static func topicArrowIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListTopicArrowIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/TopicArrowIcon"), color: theme.chatList.titleColor) + }) + } } diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index ee3b91af17..71ce323559 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -420,7 +420,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } else { if let titleContent = self.titleContent { switch titleContent { - case let .peer(peerView, _, onlineMemberCount, isScheduledMessages, _): + case let .peer(peerView, customTitle, onlineMemberCount, isScheduledMessages, _): if let peer = peerViewMainPeer(peerView) { let servicePeer = isServicePeer(peer) if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isReplies { @@ -485,7 +485,10 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { state = .info(string, .generic) } } else if let channel = peer as? TelegramChannel { - if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { + if channel.flags.contains(.isForum), customTitle != nil { + let string = NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + state = .info(string, .generic) + } else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { if memberCount == 0 { let string: NSAttributedString if case .group = channel.info { diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index 684f9ad4ea..aa6d315c0a 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -223,16 +223,19 @@ public final class EmojiStatusComponent: Component { } else { iconImage = nil } - case let .topic(title, color, size): + case let .topic(title, color, realSize): func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor]) -> UIImage? { - return generateImage(size, rotatedContext: { size, context in - context.clear(CGRect(origin: .zero, size: size)) + return generateImage(realSize, rotatedContext: { realSize, context in + context.clear(CGRect(origin: .zero, size: realSize)) context.saveGState() - let scale: CGFloat = size.width / 32.0 - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + let size = CGSize(width: 32.0, height: 32.0) + + let scale: CGFloat = realSize.width / size.width context.scaleBy(x: scale, y: scale) + + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.translateBy(x: -14.0 - UIScreenPixel, y: -14.0 - UIScreenPixel) let _ = try? drawSvgPath(context, path: "M24.1835,4.71703 C21.7304,2.42169 18.2984,0.995605 14.5,0.995605 C7.04416,0.995605 1.0,6.49029 1.0,13.2683 C1.0,17.1341 2.80572,20.3028 5.87839,22.5523 C6.27132,22.84 6.63324,24.4385 5.75738,25.7811 C5.39922,26.3301 5.00492,26.7573 4.70138,27.0861 C4.26262,27.5614 4.01347,27.8313 4.33716,27.967 C4.67478,28.1086 6.66968,28.1787 8.10952,27.3712 C9.23649,26.7392 9.91903,26.1087 10.3787,25.6842 C10.7588,25.3331 10.9864,25.1228 11.187,25.1688 C11.9059,25.3337 12.6478,25.4461 13.4075,25.5015 C13.4178,25.5022 13.4282,25.503 13.4386,25.5037 C13.7888,25.5284 14.1428,25.5411 14.5,25.5411 C21.9558,25.5411 28.0,20.0464 28.0,13.2683 C28.0,9.94336 26.5455,6.92722 24.1835,4.71703 ") @@ -269,13 +272,12 @@ public final class EmojiStatusComponent: Component { let line = CTLineCreateWithAttributedString(attributedString) let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + let lineOffset = CGPoint(x: title == "B" ? 1.0 : 0.0, y: floorToScreenPixels(realSize.height * 0.05)) + let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (realSize.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (realSize.height - lineBounds.size.height) / 2.0) + lineOffset.y) - let lineOffset = CGPoint(x: title == "B" ? 1.0 : 0.0, y: floorToScreenPixels(0.67 * scale)) - let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0) + lineOffset.y) - - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.translateBy(x: realSize.width / 2.0, y: realSize.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.translateBy(x: -realSize.width / 2.0, y: -realSize.height / 2.0) context.translateBy(x: lineOrigin.x, y: lineOrigin.y) CTLineDraw(line, context) diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 5958e9e088..740622106e 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -872,8 +872,8 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { return } var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumDemoScreen(context: context, subject: .uniqueReactions, action: { - let controller = PremiumIntroScreen(context: context, source: .reactions) + let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: { + let controller = PremiumIntroScreen(context: context, source: .animatedEmoji) replaceImpl?(controller) }) replaceImpl = { [weak controller] c in diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/Contents.json new file mode 100644 index 0000000000..adf6728753 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "forumarrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/forumarrow.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/forumarrow.pdf new file mode 100644 index 0000000000..59b91d97df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/forumarrow.pdf @@ -0,0 +1,92 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.000000 -0.350586 cm +0.000000 0.000000 0.000000 scn +0.468521 11.725403 m +0.261516 11.984160 -0.116060 12.026113 -0.374817 11.819107 c +-0.633574 11.612102 -0.675527 11.234526 -0.468521 10.975769 c +0.468521 11.725403 l +h +4.000000 6.350586 m +4.468521 5.975769 l +4.643826 6.194900 4.643826 6.506272 4.468521 6.725403 c +4.000000 6.350586 l +h +-0.468521 1.725403 m +-0.675527 1.466646 -0.633574 1.089070 -0.374817 0.882065 c +-0.116060 0.675059 0.261516 0.717011 0.468521 0.975769 c +-0.468521 1.725403 l +h +-0.468521 10.975769 m +3.531479 5.975769 l +4.468521 6.725403 l +0.468521 11.725403 l +-0.468521 10.975769 l +h +3.531479 6.725403 m +-0.468521 1.725403 l +0.468521 0.975769 l +4.468521 5.975769 l +3.531479 6.725403 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 781 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 6.000000 12.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000871 00000 n +0000000893 00000 n +0000001065 00000 n +0000001139 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1198 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 02a282e145..84aff9bbdd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -265,6 +265,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var moreInfoNavigationButton: ChatNavigationButton? private var peerView: PeerView? + private var threadInfo: EngineMessageHistoryThread.Info? private var historyStateDisposable: Disposable? @@ -520,6 +521,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var inviteRequestsContext: PeerInvitationImportersContext? private var inviteRequestsDisposable = MetaDisposable() + private var overlayTitle: String? { + var title: String? + if let threadInfo = self.threadInfo { + title = threadInfo.title + } else if let peerView = self.peerView { + if let peer = peerViewMainPeer(peerView) { + title = EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) + } + } + return title + } + public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [PeerId] = []) { let _ = ChatControllerCount.modify { value in return value + 1 @@ -3018,6 +3031,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .replyThread(replyThreadMessage) = strongSelf.chatLocation, replyThreadMessage.messageId == message.id { return .none } + if case .peer = strongSelf.chatLocation, let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) { + if message.threadId == nil { + return .none + } + } if canReplyInChat(strongSelf.presentationInterfaceState) { return .reply @@ -4408,10 +4426,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })) - self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, hasScheduledMessages, self.reportIrrelvantGeoNoticePromise.get(), displayedCountSignal) - |> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount in + let threadInfo: Signal + if let threadId = self.chatLocation.threadId { + let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: threadId) + threadInfo = context.account.postbox.combinedView(keys: [viewKey]) + |> map { views -> EngineMessageHistoryThread.Info? in + guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else { + return nil + } + guard let data = view.info?.data.get(MessageHistoryThreadData.self) else { + return nil + } + return data.info + } + |> distinctUntilChanged + } else { + threadInfo = .single(nil) + } + + self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, hasScheduledMessages, self.reportIrrelvantGeoNoticePromise.get(), displayedCountSignal, threadInfo) + |> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, threadInfo in if let strongSelf = self { - if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages { + if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages && strongSelf.threadInfo == threadInfo { return } @@ -4454,6 +4490,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let firstTime = strongSelf.peerView == nil strongSelf.peerView = peerView + strongSelf.threadInfo = threadInfo if wasGroupChannel != isGroupChannel { if let isGroupChannel = isGroupChannel, isGroupChannel { let (recentDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.recent(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) @@ -4467,7 +4504,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } if strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.peerView = peerView + strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle } var peerIsMuted = false if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { @@ -4841,9 +4878,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let firstTime = strongSelf.peerView == nil strongSelf.peerView = peerView + strongSelf.threadInfo = messageAndTopic.threadData?.info if strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.peerView = peerView + strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle } var peerDiscussionId: PeerId? @@ -5922,7 +5960,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - self.chatDisplayNode.peerView = self.peerView + self.chatDisplayNode.overlayTitle = self.overlayTitle let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) |> map { peer in diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index e91b449515..621c4035a4 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -79,9 +79,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var containerNode: ASDisplayNode? private var overlayNavigationBar: ChatOverlayNavigationBar? - var peerView: PeerView? { + var overlayTitle: String? { didSet { - self.overlayNavigationBar?.peerView = self.peerView + self.overlayNavigationBar?.title = self.overlayTitle } } @@ -234,7 +234,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { loadingPlaceholderNode = ChatLoadingPlaceholderNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners, backgroundNode: self.backgroundNode) loadingPlaceholderNode.updatePresentationInterfaceState(self.chatPresentationInterfaceState) - self.contentContainerNode.insertSubnode(loadingPlaceholderNode, aboveSubnode: self.backgroundNode) + self.backgroundNode.supernode?.insertSubnode(loadingPlaceholderNode, aboveSubnode: self.backgroundNode) self.loadingPlaceholderNode = loadingPlaceholderNode @@ -968,7 +968,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { }, close: { [weak self] in self?.dismissAsOverlay() }) - overlayNavigationBar.peerView = self.peerView + overlayNavigationBar.title = self.overlayTitle self.overlayNavigationBar = overlayNavigationBar self.containerNode?.addSubnode(overlayNavigationBar) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 41e3dd26b6..4b1b624900 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -525,6 +525,12 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let message = messages[0] + if case .peer = chatPresentationInterfaceState.chatLocation, let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) { + if message.threadId == nil { + canReply = false + } + } + if Namespaces.Message.allScheduled.contains(message.id.namespace) || message.id.peerId.isReplies { canReply = false canPin = false diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 396db92d2f..40806f2152 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -181,6 +181,17 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } } + } else { + if chatPresentationInterfaceState.interfaceState.replyMessageId == nil { + if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + return (currentPanel, nil) + } else { + let panel = ChatRestrictedInputPanelNode() + panel.context = context + panel.interfaceInteraction = interfaceInteraction + return (panel, nil) + } + } } } diff --git a/submodules/TelegramUI/Sources/ChatOverlayNavigationBar.swift b/submodules/TelegramUI/Sources/ChatOverlayNavigationBar.swift index 983e777a69..4aa1bf13af 100644 --- a/submodules/TelegramUI/Sources/ChatOverlayNavigationBar.swift +++ b/submodules/TelegramUI/Sources/ChatOverlayNavigationBar.swift @@ -22,15 +22,10 @@ final class ChatOverlayNavigationBar: ASDisplayNode { private var validLayout: CGSize? - private var peerTitle = "" - var peerView: PeerView? { + private var peerTitle: String = "" + var title: String? { didSet { - var title = "" - if let peerView = self.peerView { - if let peer = peerViewMainPeer(peerView) { - title = EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder) - } - } + let title = self.title ?? "" if self.peerTitle != title { self.peerTitle = title if let size = self.validLayout { diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 6a764539db..0709a95b20 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -49,6 +49,8 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { //TODO:localize iconImage = PresentationResourcesChat.chatPanelLockIcon(interfaceState.theme) self.textNode.attributedText = NSAttributedString(string: "The topic is closed by admin", font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) + } else if let channel = interfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum), case .peer = interfaceState.chatLocation { + self.textNode.attributedText = NSAttributedString(string: "Swipe left on a message to reply", font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) } else if let (untilDate, personal) = bannedPermission { if personal && untilDate != 0 && untilDate != Int32.max { self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RestrictedTextTimed(stringForFullDate(timestamp: untilDate, strings: interfaceState.strings, dateTimeFormat: interfaceState.dateTimeFormat)).string, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor)