mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
Bot forums
This commit is contained in:
parent
bdaf5f5a02
commit
ee749050f0
@ -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 {
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
}
|
||||
|
||||
|
@ -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: [:]
|
||||
)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import ChatMessageBubbleContentNode
|
||||
import CountrySelectionUI
|
||||
import TelegramStringFormatting
|
||||
import MergedAvatarsNode
|
||||
import ChatControllerInteraction
|
||||
import TextNodeWithEntities
|
||||
|
||||
public final class ChatUserInfoItem: ListViewItem {
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user