Bot forums

This commit is contained in:
Isaac 2025-08-19 17:24:33 +02:00
parent bdaf5f5a02
commit ee749050f0
35 changed files with 990 additions and 144 deletions

View File

@ -1049,6 +1049,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
subject = .botForumThread(forumId: linkedForumId, threadId: EngineMessage.newTopicThreadId)
}
}
subject = nil
var forumSourcePeer: Signal<EnginePeer?, NoError> = .single(nil)
if case let .savedMessagesChats(peerId) = self.location, peerId != self.context.account.peerId {

View File

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

View File

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

View File

@ -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<PeerId, Peer>()
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: [:]
)
}

View File

@ -1410,7 +1410,7 @@ public final class Transaction {
}
}
public func combineTypingDrafts(locations: Set<PeerAndThreadId>, 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<PeerAndThreadId>, 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<PeerAndThreadId>, 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<PeerAndThreadId>, 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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -113,6 +113,35 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private var appliedExpandedBlockIds: Set<Int>?
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
})
}
}

View File

@ -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",
],
)

View File

@ -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<Void, NoError>?, (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<Empty>()
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
}
}

View File

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

View File

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

View File

@ -16,7 +16,6 @@ import ChatMessageBubbleContentNode
import CountrySelectionUI
import TelegramStringFormatting
import MergedAvatarsNode
import ChatControllerInteraction
import TextNodeWithEntities
public final class ChatUserInfoItem: ListViewItem {

View File

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

View File

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

View File

@ -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<Int64, NoError> 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 {

View File

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

View File

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

View File

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

View File

@ -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<Bool, NoError>
if let peerId = chatLocation.peerId, chatLocation.threadId == EngineMessage.newTopicThreadId {
if let peerId = chatLocation.peerId, (chatLocation.threadId == EngineMessage.newTopicThreadId || chatLocation.threadId == nil) {
stopHistoryViewUpdates = Signal<Bool, NoError>.single(false)
|> then(
self.context.account.pendingMessageManager.newTopicEvents(peerId: peerId)
|> mapToSignal { event -> Signal<Bool, NoError> 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

View File

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

View File

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

View File

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