diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 006e1d571c..8d8254d63d 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1049,6 +1049,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController subject = .botForumThread(forumId: linkedForumId, threadId: EngineMessage.newTopicThreadId) } } + subject = nil var forumSourcePeer: Signal = .single(nil) if case let .savedMessagesChats(peerId) = self.location, peerId != self.context.account.peerId { diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 99044f64e6..a4f94b2dd6 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -2474,14 +2474,14 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel takenAnimation = true if abs(layout.size.height - previousApparentHeight) > CGFloat.ulpOfOne { - node.addApparentHeightAnimation(layout.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, invertOffsetDirection: invertOffsetDirection, update: { [weak node] progress, currentValue in + node.addApparentHeightAnimation(layout.size.height, duration: (node.updateAnimationDuration() ?? insertionAnimationDuration) * UIView.animationDurationFactor(), beginAt: timestamp, invertOffsetDirection: invertOffsetDirection, update: { [weak node] progress, currentValue in if let node = node { node.animateFrameTransition(progress, currentValue) } }) if node.rotated { node.transitionOffset += previousApparentHeight - layout.size.height - node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + node.addTransitionOffsetAnimation(0.0, duration: (node.updateAnimationDuration() ?? insertionAnimationDuration) * UIView.animationDurationFactor(), beginAt: timestamp) } } } @@ -2523,16 +2523,16 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel } else { node.transitionOffset += transitionOffsetDelta } - node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + node.addTransitionOffsetAnimation(0.0, duration: (node.updateAnimationDuration() ?? insertionAnimationDuration) * UIView.animationDurationFactor(), beginAt: timestamp) if previousInsets != layout.insets { node.insets = previousInsets - node.addInsetsAnimationToValue(layout.insets, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + node.addInsetsAnimationToValue(layout.insets, duration: (node.updateAnimationDuration() ?? insertionAnimationDuration) * UIView.animationDurationFactor(), beginAt: timestamp) } } } else { if !nodeFrame.size.height.isEqual(to: node.apparentHeight) { let addAnimation = previousFrame?.height != nodeFrame.size.height - node.addApparentHeightAnimation(nodeFrame.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, invertOffsetDirection: invertOffsetDirection, update: { [weak node] progress, currentValue in + node.addApparentHeightAnimation(nodeFrame.size.height, duration: (node.updateAnimationDuration() ?? insertionAnimationDuration) * UIView.animationDurationFactor(), beginAt: timestamp, invertOffsetDirection: invertOffsetDirection, update: { [weak node] progress, currentValue in if let node = node, addAnimation { node.animateFrameTransition(progress, currentValue) } @@ -2550,10 +2550,10 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel } else { node.transitionOffset += transitionOffsetDelta } - node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + node.addTransitionOffsetAnimation(0.0, duration: (node.updateAnimationDuration() ?? insertionAnimationDuration) * UIView.animationDurationFactor(), beginAt: timestamp) if previousInsets != layout.insets { node.insets = previousInsets - node.addInsetsAnimationToValue(layout.insets, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + node.addInsetsAnimationToValue(layout.insets, duration: (node.updateAnimationDuration() ?? insertionAnimationDuration) * UIView.animationDurationFactor(), beginAt: timestamp) } } else { if self.debugInfo { @@ -2562,21 +2562,21 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel if !node.rotated { if !node.insets.top.isZero { node.transitionOffset += node.insets.top - node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + node.addTransitionOffsetAnimation(0.0, duration: (node.updateAnimationDuration() ?? insertionAnimationDuration) * UIView.animationDurationFactor(), beginAt: timestamp) } } - node.animateInsertion(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), options: ListViewItemAnimationOptions(short: invertOffsetDirection)) + node.animateInsertion(timestamp, duration: (node.updateAnimationDuration() ?? insertionAnimationDuration) * UIView.animationDurationFactor(), options: ListViewItemAnimationOptions(short: invertOffsetDirection)) } } } else if animateAlpha { if previousFrame == nil { if forceAnimateInsertion { - node.animateInsertion(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), options: ListViewItemAnimationOptions(short: true)) + node.animateInsertion(timestamp, duration: (node.insertionAnimationDuration() ?? insertionAnimationDuration) * UIView.animationDurationFactor(), options: ListViewItemAnimationOptions(short: true)) } else if animateFullTransition { node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) node.layer.animateScale(from: 0.7, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) } else { - node.animateAdded(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor()) + node.animateAdded(timestamp, duration: (node.insertionAnimationDuration() ?? insertionAnimationDuration) * UIView.animationDurationFactor()) } } } diff --git a/submodules/Display/Source/ListViewItemNode.swift b/submodules/Display/Source/ListViewItemNode.swift index 7c3ff8eee7..42c6a99572 100644 --- a/submodules/Display/Source/ListViewItemNode.swift +++ b/submodules/Display/Source/ListViewItemNode.swift @@ -587,6 +587,14 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { self.setAnimationForKey("transitionOffset", animation: animation) } + open func insertionAnimationDuration() -> Double? { + return nil + } + + open func updateAnimationDuration() -> Double? { + return nil + } + open func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { } diff --git a/submodules/Postbox/Sources/MessageHistoryView.swift b/submodules/Postbox/Sources/MessageHistoryView.swift index d22de90a14..a8b000f97d 100644 --- a/submodules/Postbox/Sources/MessageHistoryView.swift +++ b/submodules/Postbox/Sources/MessageHistoryView.swift @@ -1069,23 +1069,30 @@ final class MutableMessageHistoryView: MutablePostboxView { self.typingDraft = nil return } - guard let typingDraft = postbox.currentTypingDrafts[PeerAndThreadId(peerId: peerId, threadId: threadId)] else { + if let typingDraft = postbox.currentTypingDrafts[PeerAndThreadId(peerId: peerId, threadId: threadId)] { + self.typingDraft = self.renderTypingDraft(postbox: postbox, typingDraft: typingDraft) + } else { self.typingDraft = nil - return } - self.typingDraft = self.renderTypingDraft(postbox: postbox, typingDraft: typingDraft) } private func renderTypingDraft(postbox: PostboxImpl, typingDraft: PostboxImpl.TypingDraft) -> Message? { - guard case let .single(peerId, threadId) = self.peerIds else { + guard case let .single(peerId, _) = self.peerIds else { return nil } guard let peer = postbox.peerTable.get(peerId), let author = postbox.peerTable.get(typingDraft.authorId) else { return nil } + var peers = SimpleDictionary() peers[peer.id] = peer peers[author.id] = author + + var associatedThreadInfo: Message.AssociatedThreadInfo? + if let threadId = typingDraft.threadId, let data = postbox.messageHistoryThreadIndexTable.get(peerId: peerId, threadId: threadId) { + associatedThreadInfo = postbox.seedConfiguration.decodeMessageThreadInfo(data.data) + } + return Message( stableId: typingDraft.stableId, stableVersion: typingDraft.stableVersion, @@ -1096,7 +1103,7 @@ final class MutableMessageHistoryView: MutablePostboxView { globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, - threadId: threadId, + threadId: typingDraft.threadId, timestamp: typingDraft.timestamp, flags: [.Incoming], tags: [], @@ -1112,7 +1119,7 @@ final class MutableMessageHistoryView: MutablePostboxView { associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], - associatedThreadInfo: nil, + associatedThreadInfo: associatedThreadInfo, associatedStories: [:] ) } diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 6f8bfe3c62..98264a8cd0 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1410,7 +1410,7 @@ public final class Transaction { } } - public func combineTypingDrafts(locations: Set, update: (PeerAndThreadId, (id: Int64, authorId: PeerId, timestamp: Int32, text: String, attributes: [MessageAttribute])?) -> (id: Int64, authorId: PeerId, timestamp: Int32, text: String, attributes: [MessageAttribute])?) { + public func combineTypingDrafts(locations: Set, update: (PeerAndThreadId, (id: Int64, threadId: Int64?, authorId: PeerId, timestamp: Int32, text: String, attributes: [MessageAttribute])?) -> (id: Int64, threadId: Int64?, authorId: PeerId, timestamp: Int32, text: String, attributes: [MessageAttribute])?) { assert(!self.disposed) self.postbox!.combineTypingDrafts(locations: locations, update: update) } @@ -1660,16 +1660,18 @@ final class PostboxImpl { var id: Int64 var stableId: UInt32 var stableVersion: UInt32 + var threadId: Int64? var authorId: PeerId var timestamp: Int32 var text: String var attributes: [MessageAttribute] var addedAtTimestamp: Double - init(id: Int64, stableId: UInt32, stableVersion: UInt32, authorId: PeerId, timestamp: Int32, text: String, attributes: [MessageAttribute], addedAtTimestamp: Double) { + init(id: Int64, stableId: UInt32, stableVersion: UInt32, threadId: Int64?, authorId: PeerId, timestamp: Int32, text: String, attributes: [MessageAttribute], addedAtTimestamp: Double) { self.id = id self.stableId = stableId self.stableVersion = stableVersion + self.threadId = threadId self.authorId = authorId self.timestamp = timestamp self.text = text @@ -1687,6 +1689,9 @@ final class PostboxImpl { if lhs.stableVersion != rhs.stableVersion { return false } + if lhs.threadId != rhs.threadId { + return false + } if lhs.authorId != rhs.authorId { return false } @@ -2488,12 +2493,12 @@ final class PostboxImpl { } } - fileprivate func combineTypingDrafts(locations: Set, update: (PeerAndThreadId, (id: Int64, authorId: PeerId, timestamp: Int32, text: String, attributes: [MessageAttribute])?) -> (id: Int64, authorId: PeerId, timestamp: Int32, text: String, attributes: [MessageAttribute])?) { + fileprivate func combineTypingDrafts(locations: Set, update: (PeerAndThreadId, (id: Int64, threadId: Int64?, authorId: PeerId, timestamp: Int32, text: String, attributes: [MessageAttribute])?) -> (id: Int64, threadId: Int64?, authorId: PeerId, timestamp: Int32, text: String, attributes: [MessageAttribute])?) { for location in locations { - var updated: (id: Int64, authorId: PeerId, timestamp: Int32, text: String, attributes: [MessageAttribute])? + var updated: (id: Int64, threadId: Int64?, authorId: PeerId, timestamp: Int32, text: String, attributes: [MessageAttribute])? let current = self.currentTypingDrafts[location] if let current { - updated = update(location, (current.id, current.authorId, current.timestamp, current.text, current.attributes)) + updated = update(location, (current.id, current.threadId, current.authorId, current.timestamp, current.text, current.attributes)) } else { updated = update(location, nil) } @@ -2507,7 +2512,7 @@ final class PostboxImpl { stableId = self.messageHistoryMetadataTable.getNextStableMessageIndexId() stableVersion = 100000 } - let mappedDraft = TypingDraft(id: updated.id, stableId: stableId, stableVersion: stableVersion, authorId: updated.authorId, timestamp: updated.timestamp, text: updated.text, attributes: updated.attributes, addedAtTimestamp: CFAbsoluteTimeGetCurrent()) + let mappedDraft = TypingDraft(id: updated.id, stableId: stableId, stableVersion: stableVersion, threadId: updated.threadId, authorId: updated.authorId, timestamp: updated.timestamp, text: updated.text, attributes: updated.attributes, addedAtTimestamp: CFAbsoluteTimeGetCurrent()) if self.currentTypingDrafts[location] != mappedDraft { self.currentTypingDrafts[location] = mappedDraft self.currentUpdatedTypingDrafts[location] = TypingDraftUpdate(value: mappedDraft) @@ -2520,7 +2525,7 @@ final class PostboxImpl { } private func restartTypingDraftExpirationTimerIfNeeded() { - let expirationTimeout: Double = 10.0 + let expirationTimeout: Double = 20.0 var nextTypingDraftExpirationTimestamp: Double? for (_, draft) in self.currentTypingDrafts { diff --git a/submodules/TelegramCore/Sources/ApiUtils/TypingDraftMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/TypingDraftMessageAttribute.swift new file mode 100644 index 0000000000..23a5ef6e20 --- /dev/null +++ b/submodules/TelegramCore/Sources/ApiUtils/TypingDraftMessageAttribute.swift @@ -0,0 +1,15 @@ +import Foundation +import Postbox + +public final class TypingDraftMessageAttribute: MessageAttribute { + public init() { + } + + public init(decoder: PostboxDecoder) { + preconditionFailure() + } + + public func encode(_ encoder: PostboxEncoder) { + preconditionFailure() + } +} diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 8afa6ae9c1..580736c8fb 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -3778,13 +3778,15 @@ func replayFinalState( enum LiveTypingDraftUpdate { struct Update { var id: Int64 + var threadId: Int64? var authorId: PeerId var timestamp: Int32 var text: String var entities: [MessageTextEntity] - init(id: Int64, authorId: PeerId, timestamp: Int32, text: String, entities: [MessageTextEntity]) { + init(id: Int64, threadId: Int64?, authorId: PeerId, timestamp: Int32, text: String, entities: [MessageTextEntity]) { self.id = id + self.threadId = threadId self.authorId = authorId self.timestamp = timestamp self.text = text @@ -4003,11 +4005,14 @@ func replayFinalState( let message = messages[i] let chatPeerId = message.id.peerId let key = PeerAndThreadId(peerId: chatPeerId, threadId: message.threadId) + let allKey = PeerAndThreadId(peerId: chatPeerId, threadId: nil) if liveTypingDraftUpdates[key] != nil { liveTypingDraftUpdates[key] = .cancel + liveTypingDraftUpdates[allKey] = .cancel } else if let currentDraft = transaction.getCurrentTypingDraft(location: key) { liveTypingDraftUpdates[key] = .cancel + liveTypingDraftUpdates[allKey] = .cancel messages[i] = messages[i].withUpdatedCustomStableId(currentDraft.stableId) } } @@ -4674,11 +4679,22 @@ func replayFinalState( case let .AddPeerLiveTypingDraftUpdate(peerAndThreadId, id, timestamp, authorId, text, entities): liveTypingDraftUpdates[peerAndThreadId] = .update(LiveTypingDraftUpdate.Update( id: id, + threadId: peerAndThreadId.threadId, authorId: authorId, timestamp: timestamp, text: text, entities: entities )) + if peerAndThreadId.threadId != nil { + liveTypingDraftUpdates[PeerAndThreadId(peerId: peerAndThreadId.peerId, threadId: nil)] = .update(LiveTypingDraftUpdate.Update( + id: id, + threadId: peerAndThreadId.threadId, + authorId: authorId, + timestamp: timestamp, + text: text, + entities: entities + )) + } case let .UpdatePinnedItemIds(groupId, pinnedOperation): switch pinnedOperation { case let .pin(itemId): @@ -5758,10 +5774,14 @@ func replayFinalState( case let .update(update): return ( update.id, + update.threadId, update.authorId, update.timestamp, update.text, - [TextEntitiesMessageAttribute(entities: update.entities)] + [ + TypingDraftMessageAttribute(), + TextEntitiesMessageAttribute(entities: update.entities) + ] ) case .cancel: return nil diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index 1c6a386742..b97190d5bd 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -524,7 +524,11 @@ public final class PendingMessageManager { if let forwardInfo = currentMessage.forwardInfo { storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) } - return .update(StoreMessage(id: id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: topicId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media)) + var attributes = currentMessage.attributes + if !attributes.contains(where: { $0 is ReplyMessageAttribute }) { + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: id.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: topicId)), threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)) + } + return .update(StoreMessage(id: id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: topicId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) }) if let message = transaction.getMessage(id) { result.append(message) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 0009e1e7ef..43932cf03a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -209,6 +209,11 @@ public extension TelegramEngine.EngineData.Item { return nil } peers[mainChannel.id] = EnginePeer(mainChannel) + } else if let channel = peer as? TelegramChannel, channel.isForum, let linkedBotId = channel.linkedBotId { + guard let mainChannel = view.peers[linkedBotId] else { + return nil + } + peers[mainChannel.id] = EnginePeer(mainChannel) } return EngineRenderedPeer(peerId: self.id, peers: peers, associatedMedia: view.media) diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 393234c0a0..85d8692595 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -246,6 +246,18 @@ func locallyRenderedMessage(message: StoreMessage, peers: [PeerId: Peer], associ } } } + + if let channel = peer as? TelegramChannel, let linkedBotId = channel.linkedBotId { + if let channelPeer = peers[linkedBotId] { + messagePeers[linkedBotId] = channelPeer + } + + if let threadId = message.threadId { + if let threadPeer = peers[PeerId(threadId)] { + messagePeers[threadPeer.id] = threadPeer + } + } + } } for media in message.media { diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 02cfe99123..5e3ecc60ea 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -467,6 +467,8 @@ public func peerViewMonoforumMainPeer(_ view: PeerView) -> Peer? { if let peer = peerViewMainPeer(view) { if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum), let linkedMonoforumId = channel.linkedMonoforumId { return view.peers[linkedMonoforumId] + } else if let channel = peer as? TelegramChannel, let linkedBotId = channel.linkedBotId { + return view.peers[linkedBotId] } else { return nil } @@ -507,8 +509,14 @@ public extension RenderedPeer { } var chatOrMonoforumMainPeer: Peer? { - if let channel = self.peer as? TelegramChannel, channel.flags.contains(.isMonoforum), let linkedMonoforumId = channel.linkedMonoforumId { - return self.peers[linkedMonoforumId] + if let channel = self.peer as? TelegramChannel { + if channel.flags.contains(.isMonoforum), let linkedMonoforumId = channel.linkedMonoforumId { + return self.peers[linkedMonoforumId] + } else if let linkedBotId = channel.linkedBotId { + return self.peers[linkedBotId] + } else { + return self.chatMainPeer + } } else { return self.chatMainPeer } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 9536782077..b8b8335927 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -137,7 +137,7 @@ public struct PresentationResourcesChat { public static func chatBubbleArrowFreeImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatBubbleArrowFreeImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/HeaderArrow"), color: UIColor(white: 1.0, alpha: 0.3)) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/HeaderArrow"), color: UIColor(white: 1.0, alpha: 0.4)) }) } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 7d4d317c64..60c67bc72d 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -353,6 +353,7 @@ swift_library( "//submodules/TelegramUI/Components/AudioWaveformNode", "//submodules/TelegramUI/Components/Chat/ChatBotInfoItem", "//submodules/TelegramUI/Components/Chat/ChatUserInfoItem", + "//submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem", "//submodules/TelegramUI/Components/Chat/ChatInputPanelNode", "//submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode", "//submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode", diff --git a/submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode/Sources/ChatBotStartInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode/Sources/ChatBotStartInputPanelNode.swift index f9a983a911..15fee82f2e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode/Sources/ChatBotStartInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode/Sources/ChatBotStartInputPanelNode.swift @@ -122,7 +122,7 @@ public final class ChatBotStartInputPanelNode: ChatInputPanelNode { self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: 8.0), size: buttonSize) - if !self.tooltipDismissed, let context = self.context { + if !self.tooltipDismissed, let context = self.context, let user = self.presentationInterfaceState?.renderedPeer?.peer as? TelegramUser, let botInfo = user.botInfo, !botInfo.flags.contains(.hasForum) { let absoluteFrame = self.button.view.convert(self.button.bounds, to: nil) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize()) diff --git a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift index 0f5f59ab92..aa000a8a3f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift +++ b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift @@ -19,8 +19,9 @@ public struct ChatMessageEntryAttributes: Equatable { public var isPlaying: Bool public var isCentered: Bool public var authorStoryStats: PeerStoryStats? + public var displayContinueThreadFooter: Bool - public init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?, isPlaying: Bool, isCentered: Bool, authorStoryStats: PeerStoryStats?) { + public init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?, isPlaying: Bool, isCentered: Bool, authorStoryStats: PeerStoryStats?, displayContinueThreadFooter: Bool) { self.rank = rank self.isContact = isContact self.contentTypeHint = contentTypeHint @@ -28,6 +29,7 @@ public struct ChatMessageEntryAttributes: Equatable { self.isPlaying = isPlaying self.isCentered = isCentered self.authorStoryStats = authorStoryStats + self.displayContinueThreadFooter = displayContinueThreadFooter } public init() { @@ -38,12 +40,14 @@ public struct ChatMessageEntryAttributes: Equatable { self.isPlaying = false self.isCentered = false self.authorStoryStats = nil + self.displayContinueThreadFooter = false } } public enum ChatInfoData: Equatable { case botInfo(title: String, text: String, photo: TelegramMediaImage?, video: TelegramMediaFile?) case userInfo(peer: EnginePeer, verification: PeerVerification?, registrationDate: String?, phoneCountry: String?, groupsInCommonCount: Int32) + case newThreadInfo } public enum ChatHistoryEntry: Identifiable, Comparable { @@ -90,8 +94,13 @@ public enum ChatHistoryEntry: Identifiable, Comparable { return index case let .ReplyCountEntry(index, _, _, _): return index - case .ChatInfoEntry: - return MessageIndex.absoluteLowerBound() + case let .ChatInfoEntry(infoData, _): + switch infoData { + case .newThreadInfo: + return MessageIndex.absoluteUpperBound() + default: + return MessageIndex.absoluteLowerBound() + } case .SearchEntry: return MessageIndex.absoluteLowerBound() } @@ -107,8 +116,13 @@ public enum ChatHistoryEntry: Identifiable, Comparable { return index case let .ReplyCountEntry(index, _, _, _): return index - case .ChatInfoEntry: - return MessageIndex.absoluteLowerBound() + case let .ChatInfoEntry(infoData, _): + switch infoData { + case .newThreadInfo: + return MessageIndex.absoluteUpperBound() + default: + return MessageIndex.absoluteLowerBound() + } case .SearchEntry: return MessageIndex.absoluteLowerBound() } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift index 9aa7f29a9b..4ce3474be7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift @@ -235,6 +235,8 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { iconImage = PresentationResourcesChat.messageButtonsPostApprove(theme.theme) case .suggestedPostEdit: iconImage = PresentationResourcesChat.messageButtonsPostEdit(theme.theme) + case .actionArrow: + iconImage = PresentationResourcesChat.chatBubbleArrowFreeImage(theme.theme) } tintColor = titleColor } else { @@ -453,7 +455,11 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { var titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.size.width) / 2.0), y: floor((42.0 - titleSize.size.height) / 2.0) + 1.0), size: titleSize.size) if let image = node.iconNode?.image, customInfo?.icon != nil { - titleFrame.origin.x = floorToScreenPixels((width - titleSize.size.width - image.size.width - 3.0) * 0.5) + 3.0 + image.size.width + if customInfo?.icon == .actionArrow { + titleFrame.origin.x = floorToScreenPixels((width - titleSize.size.width - image.size.width + 1.0) * 0.5) - 0.0 + } else { + titleFrame.origin.x = floorToScreenPixels((width - titleSize.size.width - image.size.width - 3.0) * 0.5) + 3.0 + image.size.width + } } titleNode.layer.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) animation.animator.updatePosition(layer: titleNode.layer, position: CGPoint(x: titleFrame.midX, y: titleFrame.midY), completion: nil) @@ -464,7 +470,11 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { if let iconNode = node.iconNode { let iconFrame: CGRect if customInfo?.icon != nil, let image = iconNode.image { - iconFrame = CGRect(x: titleFrame.minX - 3.0 - image.size.width, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - image.size.height) * 0.5) - 1.0, width: image.size.width, height: image.size.height) + if customInfo?.icon == .actionArrow { + iconFrame = CGRect(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - image.size.height) * 0.5) - 1.0, width: image.size.width, height: image.size.height) + } else { + iconFrame = CGRect(x: titleFrame.minX - 3.0 - image.size.width, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - image.size.height) * 0.5) - 1.0, width: image.size.width, height: image.size.height) + } } else { iconFrame = CGRect(x: width - 16.0, y: 4.0, width: 12.0, height: 12.0) } @@ -502,6 +512,7 @@ public final class ChatMessageActionButtonsNode: ASDisplayNode { case suggestedPostApprove case suggestedPostReject case suggestedPostEdit + case actionArrow } public struct CustomInfo { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift index 66f837bc0d..a7a021d09b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift @@ -224,6 +224,7 @@ open class ChatMessageBubbleContentNode: ASDisplayNode { public var updateIsTextSelectionActive: ((Bool) -> Void)? public var requestInlineUpdate: (() -> Void)? + public var requestFullUpdate: (() -> Void)? open var disablesClipping: Bool { return false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index ad4a5f9a3d..482cc2b1a3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -955,6 +955,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI process(node: self) } + override public func insertionAnimationDuration() -> Double? { + return nil + } + + override public func updateAnimationDuration() -> Double? { + return nil + } + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { super.animateInsertion(currentTimestamp, duration: duration, options: options) @@ -2867,6 +2875,37 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, item.controllerInteraction.presentationContext.backgroundNode, replyMarkup, [:], item.message, maximumNodeWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout + } else if item.content.firstMessageAttributes.displayContinueThreadFooter { + var buttonValue: UInt8 = 3 + let button = MemoryBuffer(data: Data(bytes: &buttonValue, count: 1)) + + let customInfos: [MemoryBuffer: ChatMessageActionButtonsNode.CustomInfo] = [ + button: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: true, + icon: .actionArrow + ), + ] + + //TODO:localize + let (minWidth, buttonsLayout) = actionButtonsLayout( + item.context, + item.presentationData.theme, + item.presentationData.chatBubbleCorners, + item.presentationData.strings, + item.controllerInteraction.presentationContext.backgroundNode, + ReplyMarkupMessageAttribute( + rows: [ + ReplyMarkupRow(buttons: [ + ReplyMarkupButton(title: "Continue last thread", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: button)) + ]) + ], + flags: [], + placeholder: nil + ), customInfos, item.message, baseWidth) + maxContentWidth = max(maxContentWidth, minWidth) + actionButtonsFinalize = buttonsLayout + + lastNodeTopPosition = .None(.Both) } var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? @@ -4462,6 +4501,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI strongSelf.internalUpdateLayout() } + contentNode.requestFullUpdate = { [weak strongSelf] in + guard let strongSelf, let item = strongSelf.item else { + return + } + + item.controllerInteraction.requestMessageUpdate(item.message.id, false) + } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 86ba2a985c..1f6409506d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -345,6 +345,15 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible if let threadInfo = content.firstMessage.associatedThreadInfo { headerSeparableThreadId = content.firstMessage.threadId headerDisplayPeer = ChatMessageDateHeader.HeaderData(contents: .thread(id: threadId, info: threadInfo)) + } else if content.firstMessage.threadId == EngineMessage.newTopicThreadId { + headerSeparableThreadId = content.firstMessage.threadId + //TODO:localize + headerDisplayPeer = ChatMessageDateHeader.HeaderData(contents: .thread(id: threadId, info: Message.AssociatedThreadInfo( + title: "New Chat", + icon: nil, + iconColor: 0, + isClosed: false + ))) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 45b69ec4de..990c1fa9d8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -113,6 +113,35 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private var appliedExpandedBlockIds: Set? private var displayContentsUnderSpoilers: (value: Bool, location: CGPoint?) = (false, nil) + private final class TextRevealAnimationState { + let fromCount: Int + let toCount: Int + let startTimestamp: Double + let duration: Double + + init(fromCount: Int, toCount: Int, startTimestamp: Double, duration: Double) { + self.fromCount = fromCount + self.toCount = toCount + self.startTimestamp = startTimestamp + self.duration = duration + } + + func fraction(timestamp: Double) -> CGFloat { + var animationFraction = (timestamp - self.startTimestamp) / self.duration + animationFraction = max(0.0, min(1.0, animationFraction)) + return animationFraction + } + + func glyphCount(timestamp: Double) -> Int { + let animationFraction = self.fraction(timestamp: timestamp) + let glyphCount = (1.0 - animationFraction) * Double(self.fromCount) + animationFraction * Double(self.toCount) + return Int(glyphCount) + } + } + + private var textRevealLink: SharedDisplayLinkDriver.Link? + private var textRevealAnimationState: TextRevealAnimationState? + override public var visibility: ListViewItemNodeVisibility { didSet { if oldValue != self.visibility { @@ -131,6 +160,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { required public init() { self.containerNode = ASDisplayNode() + self.containerNode.clipsToBounds = true self.textNode = InteractiveTextNodeWithEntities() @@ -200,12 +230,22 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + let previousItem = self.item + let textLayout = InteractiveTextNodeWithEntities.asyncLayout(self.textNode) let statusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode) let currentCachedChatMessageText = self.cachedChatMessageText let expandedBlockIds = self.expandedBlockIds let displayContentsUnderSpoilers = self.displayContentsUnderSpoilers + let currentMaxGlyphCount: Int? + if let textRevealAnimationState = self.textRevealAnimationState { + currentMaxGlyphCount = textRevealAnimationState.glyphCount(timestamp: CACurrentMediaTime()) + //print("currentMaxGlyphCount(\(textRevealAnimationState.fromCount) -> \(textRevealAnimationState.toCount)) fraction: \(textRevealAnimationState.fraction(timestamp: CACurrentMediaTime()))") + } else { + currentMaxGlyphCount = nil + } + let previousGlyphCount = self.textNode.textNode.getGlyphCount() return { item, layoutConstants, _, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -673,9 +713,32 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size) + var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom)) textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset) + let realTextFrame = textFrame + + var hasDraft = false + if item.message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { + hasDraft = true + } + var hadDraft = false + if let previousItem, previousItem.message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { + hadDraft = true + } + + var maxGlyphCount = currentMaxGlyphCount + if maxGlyphCount == nil && (hasDraft || hadDraft) { + maxGlyphCount = previousGlyphCount + } + + if let maxGlyphCount { + textFrame.size = textLayout.sizeForGlyphCount(glyphCount: maxGlyphCount) + //print("currentMaxGlyphCount: \(currentMaxGlyphCount), size: \(textFrame.size.height)") + textFrameWithoutInsets.size = CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom) + } + textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset) var suggestedBoundingWidth: CGFloat = textFrameWithoutInsets.width @@ -706,8 +769,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.cachedChatMessageText = updatedCachedChatMessageText } + var previousAnimateGlyphCount: Int? + if hasDraft || hadDraft { + previousAnimateGlyphCount = strongSelf.textNode.textNode.getGlyphCount() + } + strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview - strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: boundingSize) + animation.animator.updateFrame(layer: strongSelf.containerNode.layer, frame: CGRect(origin: CGPoint(), size: boundingSize), completion: nil) if strongSelf.appliedExpandedBlockIds != nil && strongSelf.appliedExpandedBlockIds != strongSelf.expandedBlockIds { itemApply?.setInvertOffsetDirection() @@ -764,7 +832,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } ) )) - animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil) + animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: realTextFrame, completion: nil) switch strongSelf.visibility { case .none: @@ -785,8 +853,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } strongSelf.textAccessibilityOverlayNode.frame = textFrame - //TODO:release - //strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout strongSelf.updateIsTranslating(isTranslating) @@ -897,6 +963,10 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.codeHighlightState = nil codeHighlightState.disposable.dispose() } + + if previousAnimateGlyphCount != nil || strongSelf.textRevealAnimationState != nil || hadDraft { + strongSelf.updateTextRevealAnimation(previousGlyphCount: previousAnimateGlyphCount ?? 0) + } } }) }) @@ -904,6 +974,74 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } + private func updateTextRevealAnimation(previousGlyphCount: Int) { + var fromCount = previousGlyphCount + let toCount = self.textNode.textNode.getGlyphCount() + let timestamp = CACurrentMediaTime() + if let textRevealAnimationState = self.textRevealAnimationState { + if textRevealAnimationState.toCount == toCount { + return + } + fromCount = textRevealAnimationState.glyphCount(timestamp: timestamp) + } + if fromCount == toCount { + if self.textRevealAnimationState != nil { + self.textRevealAnimationState = nil + self.textRevealLink = nil + self.textNode.textNode.updateRevealGlyphCount(count: nil) + } + return + } + + var duration: Double = Double(toCount - fromCount) / 20.0 + duration = max(0.1, min(duration, 5.0)) + + self.textRevealAnimationState = TextRevealAnimationState( + fromCount: fromCount, + toCount: toCount, + startTimestamp: timestamp, + duration: duration + ) + if self.textRevealLink == nil, self.textRevealAnimationState != nil { + var lastLineUpdateTimestamp = timestamp + self.textRevealLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in + guard let self else { + return + } + guard let textRevealAnimationState = self.textRevealAnimationState else { + self.textRevealLink = nil + return + } + let timestamp = CACurrentMediaTime() + if textRevealAnimationState.fraction(timestamp: timestamp) >= 1.0 { + self.textRevealAnimationState = nil + self.textRevealLink = nil + + self.textNode.textNode.updateRevealGlyphCount(count: nil) + self.requestFullUpdate?() + } else { + let lineUpdateTimeout = timestamp - lastLineUpdateTimestamp + + var requestUpdate = false + let glyphCount = textRevealAnimationState.glyphCount(timestamp: timestamp) + if let revealGlyphCount = self.textNode.textNode.revealGlyphCount, let cachedLayout = self.textNode.textNode.cachedLayout { + if cachedLayout.sizeForGlyphCount(glyphCount: revealGlyphCount).height != cachedLayout.sizeForGlyphCount(glyphCount: glyphCount).height { + if lineUpdateTimeout >= 0.1 { + lastLineUpdateTimestamp = timestamp + requestUpdate = true + } + } + } + self.textNode.textNode.updateRevealGlyphCount(count: glyphCount) + + if requestUpdate { + self.requestFullUpdate?() + } + } + } + } + } + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.statusNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -1569,7 +1707,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { guard let self, completed else { return } - self.containerNode.clipsToBounds = false + self.containerNode.clipsToBounds = true }) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/BUILD b/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/BUILD new file mode 100644 index 0000000000..ee2fec7f98 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/BUILD @@ -0,0 +1,31 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatNewThreadInfoItem", + module_name = "ChatNewThreadInfoItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TextFormat", + "//submodules/AccountContext", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramStringFormatting", + "//submodules/ComponentFlow", + "//submodules/Components/BundleIconComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift new file mode 100644 index 0000000000..3faae56d1b --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift @@ -0,0 +1,326 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TextFormat +import AccountContext +import WallpaperBackgroundNode +import ChatControllerInteraction +import ChatMessageBubbleContentNode +import TelegramStringFormatting +import ChatControllerInteraction +import ComponentFlow +import BundleIconComponent + +public final class ChatNewThreadInfoItem: ListViewItem { + fileprivate let controllerInteraction: ChatControllerInteraction + fileprivate let presentationData: ChatPresentationData + fileprivate let context: AccountContext + + public init( + controllerInteraction: ChatControllerInteraction, + presentationData: ChatPresentationData, + context: AccountContext + ) { + self.controllerInteraction = controllerInteraction + self.presentationData = presentationData + self.context = context + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + let configure = { + let node = ChatNewThreadInfoItemNode() + + let nodeLayout = node.asyncLayout() + let (layout, apply) = nodeLayout(self, params) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply(.None) }) + }) + } + } + if Thread.isMainThread { + async { + configure() + } + } else { + configure() + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ChatNewThreadInfoItemNode { + let nodeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = nodeLayout(self, params) + Queue.mainQueue().async { + completion(layout, { _ in + apply(animation) + }) + } + } + } + } + } +} + +public final class ChatNewThreadInfoItemNode: ListViewItemNode, ASGestureRecognizerDelegate { + public var controllerInteraction: ChatControllerInteraction? + + public let offsetContainer: ASDisplayNode + public let titleNode: TextNode + public let subtitleNode: TextNode + let arrowView: UIImageView + let iconBackground: SimpleLayer + var icon = ComponentView() + + private var theme: ChatPresentationThemeData? + + private var wallpaperBackgroundNode: WallpaperBackgroundNode? + private var backgroundContent: WallpaperBubbleBackgroundNode? + + private var absolutePosition: (CGRect, CGSize)? + + private var item: ChatNewThreadInfoItem? + + public init() { + self.offsetContainer = ASDisplayNode() + + self.iconBackground = SimpleLayer() + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.displaysAsynchronously = false + + self.subtitleNode = TextNode() + self.subtitleNode.isUserInteractionEnabled = false + self.subtitleNode.displaysAsynchronously = false + + self.arrowView = UIImageView() + + super.init(layerBacked: false, dynamicBounce: true, rotated: true) + + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + self.addSubnode(self.offsetContainer) + self.offsetContainer.addSubnode(self.titleNode) + self.offsetContainer.addSubnode(self.subtitleNode) + } + + override public func didLoad() { + super.didLoad() + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + tapRecognizer.delegate = self.wrappedGestureRecognizerDelegate + self.offsetContainer.view.addGestureRecognizer(tapRecognizer) + } + + public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer.view === self.offsetContainer.view { + let location = gestureRecognizer.location(in: self.offsetContainer.view) + if let backgroundContent = self.backgroundContent, backgroundContent.frame.contains(location) { + return true + } + return false + } + return true + } + + @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { + if let item = self.item { + item.controllerInteraction.updateInputMode { mode in + if case .none = mode { + return .text + } else { + return mode + } + } + } + } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + super.updateAbsoluteRect(rect, within: containerSize) + + self.absolutePosition = (rect, containerSize) + if let backgroundContent = self.backgroundContent { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += containerSize.height - rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } + + public func asyncLayout() -> (_ item: ChatNewThreadInfoItem, _ width: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + + let currentItem = self.item + + return { [weak self] item, params in + let themeUpdated = item.presentationData.theme !== currentItem?.presentationData.theme + + var backgroundSize = CGSize(width: 240.0, height: 0.0) + + let verticalItemInset: CGFloat = 10.0 + let horizontalInset: CGFloat = 16.0 + params.leftInset + let horizontalContentInset: CGFloat = 16.0 + let topInset: CGFloat = 15.0 + let bottomInset: CGFloat = 21.0 + let verticalSpacing: CGFloat = 6.0 + let iconBackgroundSize: CGFloat = 80.0 + let iconTextSpacing: CGFloat = 14.0 + + let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText + let subtitleColor = primaryTextColor + + backgroundSize.height += topInset + + let constrainedWidth = params.width - (horizontalInset + horizontalContentInset) * 2.0 + + //TODO:localize + let titleString = "New Thread" + let subtitleString = "Type any message to\ncreate a new thread." + + backgroundSize.height += iconBackgroundSize + backgroundSize.height += iconTextSpacing + + let titleConstrainedWidth = constrainedWidth + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: titleConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + backgroundSize.height += titleLayout.size.height + backgroundSize.height += verticalSpacing + + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: subtitleString, font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: titleConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.2, cutout: nil, insets: UIEdgeInsets())) + backgroundSize.height += subtitleLayout.size.height + backgroundSize.height += 10.0 + + backgroundSize.width = horizontalContentInset * 2.0 + max(titleLayout.size.width, subtitleLayout.size.width) + + backgroundSize.height += bottomInset + + let backgroundFrame = CGRect(origin: CGPoint(x: floor((params.width - backgroundSize.width) / 2.0), y: verticalItemInset + 4.0), size: backgroundSize) + + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: backgroundSize.height + verticalItemInset * 2.0 + 10.0), insets: UIEdgeInsets()) + return (itemLayout, { _ in + if let strongSelf = self { + strongSelf.item = item + strongSelf.theme = item.presentationData.theme + + if themeUpdated { + } + + strongSelf.offsetContainer.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize) + + var contentOriginY = backgroundFrame.origin.y + topInset + + let iconBackgroundFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width - iconBackgroundSize) * 0.5), y: contentOriginY), size: CGSize(width: iconBackgroundSize, height: iconBackgroundSize)) + strongSelf.iconBackground.frame = iconBackgroundFrame + strongSelf.iconBackground.cornerRadius = iconBackgroundSize * 0.5 + strongSelf.iconBackground.backgroundColor = (item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)).cgColor + contentOriginY += iconBackgroundSize + contentOriginY += iconTextSpacing + + if strongSelf.iconBackground.superlayer == nil { + strongSelf.offsetContainer.layer.addSublayer(strongSelf.iconBackground) + } + if strongSelf.arrowView.superview == nil { + strongSelf.offsetContainer.view.addSubview(strongSelf.arrowView) + } + + let iconComponent = AnyComponent(BundleIconComponent( + name: "Chat/Empty Chat/ChannelMessages", + tintColor: primaryTextColor + )) + let iconSize = strongSelf.icon.update( + transition: .immediate, + component: iconComponent, + environment: {}, + containerSize: CGSize(width: 50.0, height: 50.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floor((iconBackgroundFrame.width - iconSize.width) * 0.5), y: iconBackgroundFrame.minY + floor((iconBackgroundFrame.height - iconSize.height) * 0.5)), size: iconSize) + if let iconView = strongSelf.icon.view { + if iconView.superview == nil { + strongSelf.offsetContainer.view.addSubview(iconView) + } + iconView.frame = iconFrame + } + + let _ = titleApply() + let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - titleLayout.size.width) / 2.0), y: contentOriginY), size: titleLayout.size) + strongSelf.titleNode.frame = titleFrame + contentOriginY += titleLayout.size.height + contentOriginY += verticalSpacing + + let _ = subtitleApply() + let subtitleFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - subtitleLayout.size.width) / 2.0), y: contentOriginY), size: subtitleLayout.size) + strongSelf.subtitleNode.frame = subtitleFrame + contentOriginY += subtitleLayout.size.height + contentOriginY += 20.0 + + if strongSelf.arrowView.image == nil { + strongSelf.arrowView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: .white)?.withRenderingMode(.alwaysTemplate) + } + strongSelf.arrowView.tintColor = primaryTextColor.withMultipliedAlpha(0.5) + if let image = strongSelf.arrowView.image { + let scaleFactor: CGFloat = 0.8 + let imageSize = CGSize(width: floor(image.size.width * scaleFactor), height: floor(image.size.height * scaleFactor)) + let arrowFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - imageSize.width) / 2.0), y: backgroundFrame.minY + backgroundFrame.height - 8.0 - imageSize.height), size: imageSize) + strongSelf.arrowView.frame = arrowFrame + } + + if strongSelf.backgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + strongSelf.backgroundContent = backgroundContent + strongSelf.offsetContainer.insertSubnode(backgroundContent, at: 0) + } + + if let backgroundContent = strongSelf.backgroundContent { + backgroundContent.cornerRadius = item.presentationData.chatBubbleCorners.mainRadius + backgroundContent.frame = backgroundFrame + if let (rect, containerSize) = strongSelf.absolutePosition { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += containerSize.height - rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } + } + }) + } + } + + override public func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) { + /*if height.isLessThanOrEqualTo(0.0) { + transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(), size: self.offsetContainer.bounds.size)) + } else { + transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: -floorToScreenPixels(height / 2.0)), size: self.offsetContainer.bounds.size)) + }*/ + } + + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) + } + + override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + let result = super.point(inside: point, with: event) + let extra = self.offsetContainer.frame.contains(point) + return result || extra + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 2a2bea029b..88bf9e57a3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -522,7 +522,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess accountPeer: nil ) - let entryAttributes = ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil) + let entryAttributes = ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false) let items = self.messages.map { message -> ChatMessageBubbleContentItem in return ChatMessageBubbleContentItem( diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift index 881f021c3a..e9c96f72d5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift @@ -392,9 +392,8 @@ public final class ChatSideTopicsPanel: Component { } else { avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: iconSize) } - } else if topicId == EngineMessage.newTopicThreadId { - avatarIconContent = .image(image: PresentationResourcesChatList.newTopicTemplateIcon(component.theme), tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor) } else { + //newTopicTemplateIcon avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicTemplateIcon(component.theme), tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor) } } @@ -437,9 +436,6 @@ public final class ChatSideTopicsPanel: Component { let _ = topicId if let threadData = component.item.item.threadData { titleText = threadData.info.title - } else if topicId == EngineMessage.newTopicThreadId { - //TODO:localize - titleText = "New Chat" } else { titleText = " " } @@ -835,9 +831,8 @@ public final class ChatSideTopicsPanel: Component { } else { avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: iconSize) } - } else if topicId == EngineMessage.newTopicThreadId { - avatarIconContent = .image(image: PresentationResourcesChatList.newTopicTemplateIcon(component.theme), tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor) } else { + //newTopicTemplateIcon avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicTemplateIcon(component.theme), tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor) } } @@ -874,9 +869,6 @@ public final class ChatSideTopicsPanel: Component { let _ = topicId if let threadData = component.item.item.threadData { titleText = threadData.info.title - } else if topicId == EngineMessage.newTopicThreadId { - //TODO:localize - titleText = "New Chat" } else { titleText = " " } @@ -1258,7 +1250,7 @@ public final class ChatSideTopicsPanel: Component { let titleText: String if case .botForum = component.kind { //TODO:localize - titleText = "General" + titleText = "New Chat" } else { titleText = component.strings.Chat_InlineTopicMenu_AllTab } @@ -1396,7 +1388,7 @@ public final class ChatSideTopicsPanel: Component { let titleText: String if case .botForum = component.kind { //TODO:localize - titleText = "General" + titleText = "New Chat" } else { titleText = component.strings.Chat_InlineTopicMenu_AllTab } @@ -1735,7 +1727,7 @@ public final class ChatSideTopicsPanel: Component { self.itemsDisposable = (threadListSignal |> deliverOnMainQueue).startStrict(next: { [weak self] peerId, chatList in - guard let self, let component = self.component else { + guard let self, let _ = self.component else { return } @@ -1745,46 +1737,9 @@ public final class ChatSideTopicsPanel: Component { self.rawItems.removeAll() for item in chatList.items.reversed() { - if case .botForum = component.kind, case let .forum(topicId) = item.id, topicId == 1 { - #if DEBUG && false - #else - continue - #endif - } self.rawItems.append(Item(item: item)) } - if case .botForum = component.kind, !self.rawItems.contains(where: { item in - if case let .forum(topicId) = item.id { - return topicId == EngineMessage.newTopicThreadId - } else { - return false - } - }) { - self.rawItems.insert(Item(item: EngineChatList.Item( - id: .forum(EngineMessage.newTopicThreadId), - index: EngineChatList.Item.Index.forum(pinnedIndex: .none, timestamp: Int32.max - 1, threadId: EngineMessage.newTopicThreadId, namespace: Namespaces.Message.Local, id: 1), - messages: [], - readCounters: nil, - isMuted: false, - draft: nil, - threadData: nil, - renderedPeer: EngineRenderedPeer(peerId: peerId, peers: [:], associatedMedia: [:]), - presence: nil, - hasUnseenMentions: false, - hasUnseenReactions: false, - forumTopicData: nil, - topForumTopicItems: [], - hasFailed: false, - isContact: false, - autoremoveTimeout: nil, - storyStats: nil, - displayAsTopicList: false, - isPremiumRequiredToMessage: false, - mediaDraftContentType: nil - )), at: 0) - } - if self.reorderingItems != nil { self.reorderingItems = self.rawItems } diff --git a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift index c8d1b2c137..c9bab92dc5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift @@ -16,7 +16,6 @@ import ChatMessageBubbleContentNode import CountrySelectionUI import TelegramStringFormatting import MergedAvatarsNode -import ChatControllerInteraction import TextNodeWithEntities public final class ChatUserInfoItem: ListViewItem { diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index e11c583606..465d5e2eeb 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -1018,6 +1018,30 @@ public final class InteractiveTextNodeLayout: NSObject { } return nil } + + public func sizeForGlyphCount(glyphCount: Int) -> CGSize { + var height: CGFloat = 0.0 + if !self.segments.isEmpty, let line = self.segments[0].lines.first { + height = line.frame.maxY + } + var count = 0 + for segment in self.segments { + for line in segment.lines { + if count >= glyphCount { + break + } + let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray + for run in glyphRuns { + let run = run as! CTRun + let glyphCount = CTRunGetGlyphCount(run) + count += Int(glyphCount) + } + height = max(height, line.frame.maxY) + } + } + height += self.insets.top + self.insets.bottom + 2.0 + return CGSize(width: self.size.width + self.insets.left + self.insets.right, height: ceil(height)) + } } private func addSpoiler(line: InteractiveTextNodeLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int) { @@ -1144,6 +1168,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn } public internal(set) var cachedLayout: InteractiveTextNodeLayout? + public internal(set) var revealGlyphCount: Int? public var renderContentTypes: RenderContentTypes = .all private var contentItemLayers: [Int: TextContentItemLayer] = [:] @@ -2068,6 +2093,65 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn }) } } + + public func getGlyphCount() -> Int { + guard let cachedLayout = self.cachedLayout else { + return 0 + } + + var count: Int = 0 + + for segment in cachedLayout.segments { + for line in segment.lines { + let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray + for run in glyphRuns { + let run = run as! CTRun + let glyphCount = CTRunGetGlyphCount(run) + count += Int(glyphCount) + } + } + } + + return count + } + + public func updateRevealGlyphCount(count: Int?) { + guard let cachedLayout = self.cachedLayout else { + return + } + + self.revealGlyphCount = count + + if let count { + var nextItemId = 0 + var currentCount = 0 + for segment in cachedLayout.segments { + let itemId = nextItemId + nextItemId += 1 + + var segmentGlyphCount = 0 + for line in segment.lines { + let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray + for run in glyphRuns { + let run = run as! CTRun + let glyphCount = CTRunGetGlyphCount(run) + segmentGlyphCount += Int(glyphCount) + } + } + + let segmentInnerCount = min(max(0, count - currentCount), segmentGlyphCount) + if let item = self.contentItemLayers[itemId] { + item.updateMaxGlyphDrawCount(value: segmentInnerCount) + } + + currentCount += segmentGlyphCount + } + } else { + for (_, item) in self.contentItemLayers { + item.updateMaxGlyphDrawCount(value: nil) + } + } + } } final class TextContentItem { @@ -2148,11 +2232,13 @@ final class TextContentItemLayer: SimpleLayer { let size: CGSize let item: TextContentItem let mask: RenderMask? + let maxGlyphDrawCount: Int? - init(size: CGSize, item: TextContentItem, mask: RenderMask?) { + init(size: CGSize, item: TextContentItem, mask: RenderMask?, maxGlyphDrawCount: Int?) { self.size = size self.item = item self.mask = mask + self.maxGlyphDrawCount = maxGlyphDrawCount super.init() } @@ -2232,6 +2318,8 @@ final class TextContentItemLayer: SimpleLayer { let offset = params.item.contentOffset let alignment: NSTextAlignment = .left + var drawnGlyphCount = 0 + for i in 0 ..< params.item.segment.lines.count { let line = params.item.segment.lines[i] @@ -2261,6 +2349,15 @@ final class TextContentItemLayer: SimpleLayer { for run in glyphRuns { let run = run as! CTRun let glyphCount = CTRunGetGlyphCount(run) + + var runDrawGlyphCount = glyphCount + if let maxGlyphDrawCount = params.maxGlyphDrawCount { + if drawnGlyphCount >= maxGlyphDrawCount { + break + } + runDrawGlyphCount = CFIndex(max(0, min(Int(glyphCount), maxGlyphDrawCount - drawnGlyphCount))) + } + let attributes = CTRunGetAttributes(run) as NSDictionary if attributes["Attribute__EmbeddedItem"] != nil { continue @@ -2310,12 +2407,14 @@ final class TextContentItemLayer: SimpleLayer { let stringRange = CTRunGetStringRange(run) if line.attachments.contains(where: { $0.range.contains(stringRange.location) }) { } else { - CTRunDraw(run, context, CFRangeMake(0, glyphCount)) + CTRunDraw(run, context, CFRangeMake(0, runDrawGlyphCount)) } } else { - CTRunDraw(run, context, CFRangeMake(0, glyphCount)) + CTRunDraw(run, context, CFRangeMake(0, runDrawGlyphCount)) } + drawnGlyphCount += Int(glyphCount) + if fixDoubleEmoji { context.setBlendMode(.normal) } @@ -2464,6 +2563,8 @@ final class TextContentItemLayer: SimpleLayer { private var isAnimating: Bool = false private var currentContentMask: RenderMask? + private var maxGlyphDrawCount: Int? + init(displaysAsynchronously: Bool) { self.renderNode = RenderNode() self.renderNode.displaysAsynchronously = displaysAsynchronously @@ -2483,6 +2584,17 @@ final class TextContentItemLayer: SimpleLayer { fatalError("init(coder:) has not been implemented") } + func updateMaxGlyphDrawCount(value: Int?) { + if self.maxGlyphDrawCount != value { + self.maxGlyphDrawCount = value + + if let renderParams = self.renderNode.params { + self.renderNode.params = RenderParams(size: renderParams.size, item: renderParams.item, mask: renderParams.mask, maxGlyphDrawCount: self.maxGlyphDrawCount) + self.renderNode.displayImmediately() + } + } + } + func update( params: Params, animation: ListViewItemUpdateAnimation, @@ -2735,7 +2847,7 @@ final class TextContentItemLayer: SimpleLayer { self.currentContentMask = contentMask - self.renderNode.params = RenderParams(size: contentFrame.size, item: params.item, mask: staticContentMask) + self.renderNode.params = RenderParams(size: contentFrame.size, item: params.item, mask: staticContentMask, maxGlyphDrawCount: self.maxGlyphDrawCount) if synchronously { if let spoilerExpandRect, animation.isAnimated { let localSpoilerExpandRect = spoilerExpandRect.offsetBy(dx: -self.renderNode.frame.minX, dy: -self.renderNode.frame.minY) diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 2a94368553..357c09f126 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -854,7 +854,7 @@ public final class PeerListItemComponent: Component { } else if peer.isFake { statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_FakeAccount.uppercased()) } else if let emojiStatus = peer.emojiStatus { - statusIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(2)) + statusIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(0)) if let color = emojiStatus.color { particleColor = UIColor(rgb: UInt32(bitPattern: color)) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 61b6ddc25c..2acb5d1509 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -167,6 +167,7 @@ extension ChatControllerImpl { } if let historyNodeData = contentData.state.historyNodeData { + self.subject = nil self.updateChatLocationToOther(chatLocation: historyNodeData.chatLocation) return } else if case let .botForumThread(linkedForumId, threadId) = self.subject { @@ -210,7 +211,7 @@ extension ChatControllerImpl { } }) - if self.newTopicEventsDisposable == nil, let peerId = chatLocation.peerId, chatLocation.threadId == EngineMessage.newTopicThreadId { + if self.newTopicEventsDisposable == nil, let peerId = chatLocation.peerId, (chatLocation.threadId == EngineMessage.newTopicThreadId || chatLocation.threadId == nil) { self.newTopicEventsDisposable = (self.context.account.pendingMessageManager.newTopicEvents(peerId: peerId) |> mapToSignal { event -> Signal in if case let .didMove(fromThreadId, toThreadId) = event { @@ -225,7 +226,11 @@ extension ChatControllerImpl { guard let self else { return } - self.updateInitialChatBotForumLocationThread(linkedForumId: peerId, threadId: threadId) + if chatLocation.peerId != peerId { + self.updateInitialChatBotForumLocationThread(linkedForumId: peerId, threadId: threadId) + } else { + self.updateChatLocationThread(threadId: threadId, animationDirection: .right) + } }) } }) @@ -846,21 +851,6 @@ extension ChatControllerImpl { self.reloadCachedData() - self.historyStateDisposable = self.chatDisplayNode.historyNode.historyState.get().startStrict(next: { [weak self] state in - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: strongSelf.isViewLoaded && strongSelf.view.window != nil, { - $0.updatedChatHistoryState(state) - }) - - if let botStart = strongSelf.botStart, case let .loaded(isEmpty, _) = state { - strongSelf.botStart = nil - if !isEmpty { - strongSelf.startBot(botStart.payload) - } - } - } - }) - if self.context.sharedContext.immediateExperimentalUISettings.crashOnLongQueries { let _ = (self.ready.get() |> filter({ $0 }) @@ -4570,6 +4560,22 @@ extension ChatControllerImpl { } func setupChatHistoryNode(historyNode: ChatHistoryListNodeImpl) { + self.historyStateDisposable?.dispose() + self.historyStateDisposable = historyNode.historyState.get().startStrict(next: { [weak self] state in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: strongSelf.isViewLoaded && strongSelf.view.window != nil, { + $0.updatedChatHistoryState(state) + }) + + if let botStart = strongSelf.botStart, case let .loaded(isEmpty, _) = state { + strongSelf.botStart = nil + if !isEmpty { + strongSelf.startBot(botStart.payload) + } + } + } + }) + do { let peerId = self.chatLocation.peerId if let subject = self.subject, case .scheduledMessages = subject { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 5db270e5de..3689e60d60 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2516,6 +2516,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } return + } else if !message.attributes.contains(where: { attribute in + if let attribute = attribute as? ReplyMarkupMessageAttribute, !attribute.rows.isEmpty { + return true + } else { + return false + } + }), let data = data?.makeData(), data.count == 1 { + let buttonType = data.withUnsafeBytes { buffer -> UInt8 in + return buffer.baseAddress!.assumingMemoryBound(to: UInt8.self).pointee + } + if buttonType == 3 { + if let threadId = message.threadId { + strongSelf.updateChatLocationThread(threadId: threadId, animationDirection: .right) + return + } + } } let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) @@ -8099,10 +8115,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func transformEnqueueMessages(_ messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil, postpone: Bool = false) -> [EnqueueMessage] { + var defaultThreadId: Int64? var defaultReplyMessageSubject: EngineMessageReplySubject? switch self.chatLocation { case .peer: - break + if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.linkedBotId != nil { + defaultThreadId = EngineMessage.newTopicThreadId + } case let .replyThread(replyThreadMessage): if let effectiveMessageId = replyThreadMessage.effectiveMessageId { defaultReplyMessageSubject = EngineMessageReplySubject(messageId: effectiveMessageId, quote: nil, todoItemId: nil) @@ -8127,6 +8146,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break } } + if let defaultThreadId { + var updateThread = false + switch message { + case let .message(_, _, _, _, threadId, replyToMessageId, _, _, _, _): + if threadId == nil && replyToMessageId == nil { + updateThread = true + } + case let .forward(_, threadId, _, _, _): + if threadId == nil { + updateThread = true + } + } + if updateThread { + message = message.withUpdatedThreadId(defaultThreadId) + } + } if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.peerId == self.context.account.peerId { switch message { @@ -10125,10 +10160,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var currentChatSwitchDirection: ChatControllerAnimateInnerChatSwitchDirection? func updateChatLocationToOther(chatLocation: ChatLocation) { - if self.isUpdatingChatLocationThread { - return - } - self.saveInterfaceState() self.chatDisplayNode.dismissTextInput() @@ -10269,11 +10300,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G )) } } else { - if let channel = peer as? TelegramChannel, let linkedBotId = channel.linkedBotId { + /*if let channel = peer as? TelegramChannel, let linkedBotId = channel.linkedBotId { updatedChatLocation = .peer(id: linkedBotId) - } else { + } else {*/ updatedChatLocation = .peer(id: peerId) - } + //} } let navigationSnapshot = self.chatTitleView?.prepareSnapshotState() diff --git a/submodules/TelegramUI/Sources/ChatControllerContentData.swift b/submodules/TelegramUI/Sources/ChatControllerContentData.swift index 7780e5791d..886e38e849 100644 --- a/submodules/TelegramUI/Sources/ChatControllerContentData.swift +++ b/submodules/TelegramUI/Sources/ChatControllerContentData.swift @@ -578,6 +578,16 @@ extension ChatControllerImpl { } else { strongSelf.state.chatTitleContent = .custom(channel.debugDisplayTitle, nil, true) } + } else if let channel = peer as? TelegramChannel, let linkedBotId = channel.linkedBotId, let mainPeer = peerView.peers[linkedBotId] { + strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData( + peerId: mainPeer.id, + peer: mainPeer, + isContact: false, + isSavedMessages: false, + notificationSettings: nil, + peerPresences: [:], + cachedData: nil + ), customTitle: nil, customSubtitle: nil, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: nil, customMessageCount: nil, isEnabled: true) } else { strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, customSubtitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo) @@ -825,6 +835,13 @@ extension ChatControllerImpl { } disallowedGifts = cachedData.disallowedGifts } + + if chatLocation.threadId == nil, case let .known(value) = cachedData.linkedBotChannelId, let value, chatLocation.peerId != value { + strongSelf.state.historyNodeData = HistoryNodeData( + chatLocation: .peer(id: value), + chatLocationContextHolder: Atomic(value: nil) + ) + } } else if let cachedData = peerView.cachedData as? CachedGroupData { var invitedBy: Peer? if let invitedByPeerId = cachedData.invitedBy { diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 9add155f1d..38a085cdaf 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -131,8 +131,20 @@ func chatHistoryEntriesForView( if case let .replyThread(replyThreadMessage) = location, replyThreadMessage.isForumPost { for media in message.media { - if let action = media as? TelegramMediaAction, case .topicCreated = action.action { - continue loop + if let action = media as? TelegramMediaAction { + if case .topicCreated = action.action { + continue loop + } else if case .groupCreated = action.action { + var chatPeer: Peer? + for entry in view.additionalData { + if case let .peer(_, peer) = entry { + chatPeer = peer + } + } + if let channel = chatPeer as? TelegramChannel, (channel.isMonoForum || channel.linkedBotId != nil) { + continue loop + } + } } } } else if case .peer = location { @@ -144,7 +156,7 @@ func chatHistoryEntriesForView( chatPeer = peer } } - if let channel = chatPeer as? TelegramChannel, channel.isMonoForum { + if let channel = chatPeer as? TelegramChannel, (channel.isMonoForum || channel.linkedBotId != nil) { continue loop } } @@ -223,7 +235,7 @@ func chatHistoryEntriesForView( isCentered = link.isCentered } - let attributes = ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: isCentered, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }) + let attributes = ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: isCentered, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false) let groupStableId = currentState.messageGroupStableId(messageStableId: message.stableId, groupId: messageGroupingKey, isLocal: Namespaces.Message.allLocal.contains(message.id.namespace)) var found = false @@ -269,7 +281,7 @@ func chatHistoryEntriesForView( isCentered = link.isCentered } - entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: isCentered, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }))) + entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: isCentered, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false))) } } else { let selection: ChatHistoryMessageSelection @@ -279,7 +291,7 @@ func chatHistoryEntriesForView( selection = .none } - entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }))) + entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false))) } } @@ -299,6 +311,30 @@ func chatHistoryEntriesForView( entries = flatEntries } + var addBotForumHeader = false + if location.threadId == nil, let channel = chatPeer as? TelegramChannel, channel.linkedBotId != nil, !entries.isEmpty, !view.holeEarlier, !view.isLoading { + addBotForumHeader = true + outer: for i in (0 ..< entries.count).reversed() { + switch entries[i] { + case let .MessageEntry(message, presentationData, isRead, location, selection, attributes): + if message.threadId == nil { + continue outer + } + for media in message.media { + if let _ = media as? TelegramMediaAction { + continue outer + } + } + var attributes = attributes + attributes.displayContinueThreadFooter = true + entries[i] = .MessageEntry(message, presentationData, isRead, location, selection, attributes) + break outer + default: + break + } + } + } + let insertPendingProcessingMessage: ([Message], Int) -> Void = { messages, index in let serviceMessage = Message( stableId: UInt32.max - messages[0].stableId, @@ -326,7 +362,7 @@ func chatHistoryEntriesForView( associatedThreadInfo: nil, associatedStories: [:] ) - entries.insert(.MessageEntry(serviceMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: index) + entries.insert(.MessageEntry(serviceMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false)), at: index) } for i in (0 ..< entries.count).reversed() { @@ -366,7 +402,7 @@ func chatHistoryEntriesForView( } } if let insertAtPosition { - entries.insert(.MessageEntry(joinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: insertAtPosition) + entries.insert(.MessageEntry(joinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false)), at: insertAtPosition) } } } @@ -428,12 +464,12 @@ func chatHistoryEntriesForView( if messages.count > 1, let groupingKey = messages[0].groupingKey { var groupMessages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = [] for message in messages { - groupMessages.append((message, false, .none, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }), nil)) + groupMessages.append((message, false, .none, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false), nil)) } entries.insert(.MessageGroupEntry(groupingKey, groupMessages, presentationData), at: 0) } else { if !hasTopicCreated { - entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id], isPlaying: false, isCentered: false, authorStoryStats: messages[0].author.flatMap { view.peerStoryStats[$0.id] })), at: 0) + entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id], isPlaying: false, isCentered: false, authorStoryStats: messages[0].author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false)), at: 0) } } @@ -507,7 +543,7 @@ func chatHistoryEntriesForView( associatedThreadInfo: nil, associatedStories: [:] ) - entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: 0) + entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false)), at: 0) } if let chatPeer, let nameChangeDate = peerStatusSettings.nameChangeDate, nameChangeDate > 0 { @@ -547,7 +583,7 @@ func chatHistoryEntriesForView( associatedThreadInfo: nil, associatedStories: [:] ) - entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: 0) + entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false)), at: 0) } if let peer = chatPeer.flatMap(EnginePeer.init) { @@ -600,7 +636,7 @@ func chatHistoryEntriesForView( if !dynamicAdMessages.isEmpty { assert(entries.sorted() == entries) for message in dynamicAdMessages { - entries.append(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil))) + entries.append(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false))) } entries.sort() } @@ -635,7 +671,7 @@ func chatHistoryEntriesForView( associatedStories: message.associatedStories ) nextAdMessageId += 1 - entries.append(.MessageEntry(updatedMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil))) + entries.append(.MessageEntry(updatedMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false))) } } } else if includeSearchEntry { @@ -645,6 +681,9 @@ func chatHistoryEntriesForView( } } } + if addBotForumHeader { + entries.append(.ChatInfoEntry(.newThreadInfo, presentationData)) + } if includeEmbeddedSavedChatInfo, let peerId = location.peerId { if !view.isLoading && view.laterId == nil { let string = presentationData.strings.Chat_SavedMessagesTabInfoText @@ -697,7 +736,7 @@ func chatHistoryEntriesForView( associatedThreadInfo: nil, associatedStories: [:] ) - entries.append(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil))) + entries.append(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false))) } } @@ -754,7 +793,7 @@ func chatHistoryEntriesForView( associatedThreadInfo: nil, associatedStories: [:] ) - entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: 0) + entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false)), at: 0) } } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index cbaa6b078c..b4425f0cd4 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -37,6 +37,7 @@ import ChatControllerInteraction import DustEffect import UrlHandling import TextFormat +import ChatNewThreadInfoItem struct ChatTopVisibleMessageRange: Equatable { var lowerBound: MessageIndex @@ -262,6 +263,8 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca item = ChatBotInfoItem(title: title, text: text, photo: photo, video: video, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context) case let .userInfo(peer, verification, registrationDate, phoneCountry, groupsInCommonCount): item = ChatUserInfoItem(peer: peer, verification: verification, registrationDate: registrationDate, phoneCountry: phoneCountry, groupsInCommonCount: groupsInCommonCount, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context) + case .newThreadInfo: + item = ChatNewThreadInfoItem(controllerInteraction: controllerInteraction, presentationData: presentationData, context: context) } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .SearchEntry(theme, strings): @@ -319,6 +322,8 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca item = ChatBotInfoItem(title: title, text: text, photo: photo, video: video, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context) case let .userInfo(peer, verification, registrationDate, phoneCountry, groupsInCommonCount): item = ChatUserInfoItem(peer: peer, verification: verification, registrationDate: registrationDate, phoneCountry: phoneCountry, groupsInCommonCount: groupsInCommonCount, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context) + case .newThreadInfo: + item = ChatNewThreadInfoItem(controllerInteraction: controllerInteraction, presentationData: presentationData, context: context) } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .SearchEntry(theme, strings): @@ -1747,12 +1752,12 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto |> distinctUntilChanged let stopHistoryViewUpdates: Signal - if let peerId = chatLocation.peerId, chatLocation.threadId == EngineMessage.newTopicThreadId { + if let peerId = chatLocation.peerId, (chatLocation.threadId == EngineMessage.newTopicThreadId || chatLocation.threadId == nil) { stopHistoryViewUpdates = Signal.single(false) |> then( self.context.account.pendingMessageManager.newTopicEvents(peerId: peerId) |> mapToSignal { event -> Signal in - if case .willMove(EngineMessage.newTopicThreadId, _) = event { + if case .willMove = event { return .single(true) } else { return .never() @@ -4478,6 +4483,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto currentMessage = messages.first?.0 } break loop + } else if case .ChatInfoEntry = entry { + break loop } } index += 1 diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index eb06530a15..9463cee75c 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -122,6 +122,22 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present } } + if let channel = presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForum, channel.linkedBotId != nil, case .peer = presentationInterfaceState.chatLocation { + let displaySearch = hasMessages + + if displaySearch { + if case .search(false) = currentButton?.action { + return currentButton + } else { + let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector) + buttonItem.accessibilityLabel = strings.Conversation_Search + return ChatNavigationButton(action: .search(hasTags: false), buttonItem: buttonItem) + } + } else { + return nil + } + } + if let channel = presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, let moreInfoNavigationButton = moreInfoNavigationButton { if case .replyThread = presentationInterfaceState.chatLocation { } else { diff --git a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift index dee8e42844..de106edb35 100644 --- a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift @@ -191,7 +191,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { if case .everything = search.domain { if let _ = params.interfaceState.renderedPeer?.peer as? TelegramGroup { canSearchMembers = true - } else if let peer = params.interfaceState.renderedPeer?.peer as? TelegramChannel, case .group = peer.info, !peer.isMonoForum { + } else if let peer = params.interfaceState.renderedPeer?.peer as? TelegramChannel, case .group = peer.info, !peer.isMonoForum, peer.linkedBotId == nil { canSearchMembers = true } } else { diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 3a6d8dc44e..fa0851362f 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1919,6 +1919,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } else if let channel = peer as? TelegramChannel, channel.isForumOrMonoForum, let forumTopicData = interfaceState.forumTopicData { if let replyMessage = interfaceState.replyMessage, let threadInfo = replyMessage.associatedThreadInfo { placeholder = interfaceState.strings.Chat_InputPlaceholderReplyInTopic(threadInfo.title).string + } else if let _ = channel.linkedBotId, interfaceState.chatLocation.threadId == nil { + placeholder = interfaceState.strings.Conversation_InputTextPlaceholder } else { placeholder = interfaceState.strings.Chat_InputPlaceholderMessageInTopic(forumTopicData.title).string }