Comments updates

This commit is contained in:
Ali 2020-09-25 17:47:42 +04:00
parent 1d8dd6202b
commit 6a0d0c1481
34 changed files with 4880 additions and 4143 deletions

View File

@ -5777,8 +5777,21 @@ Any member of this group will be able to see messages in the channel.";
"Conversation.ContextViewThread" = "View Thread";
"Conversation.ViewReply" = "View Reply";
"Conversation.MessageViewComments_1" = "%@ Comment";
"Conversation.MessageViewComments_any" = "%@ Comments";
"Conversation.MessageViewComments_1" = "[%@]Comment";
"Conversation.MessageViewComments_any" = "[%@]Comments";
"Conversation.MessageViewCommentsFormat" = "%1$@ %2$@";
"Conversation.TitleCommentsEmpty" = "Comments";
"Conversation.TitleComments_1" = "[%@]Comment";
"Conversation.TitleComments_any" = "[%@]Comments";
"Conversation.TitleCommentsFormat" = "%1$@ %2$@";
"Conversation.TitleRepliesEmpty" = "Replies";
"Conversation.TitleReplies_1" = "[%@]Comment";
"Conversation.TitleReplies_any" = "[%@]Comments";
"Conversation.TitleRepliesFormat" = "%1$@ %2$@";
"Conversation.MessageLeaveComment" = "Leave a Comment";
"Conversation.MessageLeaveCommentShort" = "Comment";
@ -5788,8 +5801,6 @@ Any member of this group will be able to see messages in the channel.";
"Conversation.InputTextPlaceholderReply" = "Reply";
"Conversation.InputTextPlaceholderComment" = "Comment";
"Conversation.TitleComments_1" = "%@ Comment";
"Conversation.TitleComments_any" = "%@ Comments";
"Conversation.TitleNoComments" = "Comments";
"Conversation.ContextMenuBlock" = "Block User";

View File

@ -0,0 +1,22 @@
load("//Config:buck_rule_macros.bzl", "static_library")
static_library(
name = "AnimatedAvatarSetNode",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/Display:Display#shared",
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
"//submodules/Postbox:Postbox#shared",
"//submodules/TelegramCore:TelegramCore#shared",
"//submodules/SyncCore:SyncCore#shared",
"//submodules/AccountContext:AccountContext",
"//submodules/AvatarNode:AvatarNode",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
],
)

View File

@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimatedAvatarSetNode",
module_name = "AnimatedAvatarSetNode",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/Display:Display",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SyncCore:SyncCore",
"//submodules/AccountContext:AccountContext",
"//submodules/AvatarNode:AvatarNode",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,206 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import AvatarNode
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import AccountContext
public final class AnimatedAvatarSetContext {
public final class Content {
fileprivate final class Item {
fileprivate struct Key: Hashable {
var peerId: PeerId
}
fileprivate let peer: Peer
fileprivate init(peer: Peer) {
self.peer = peer
}
}
fileprivate var items: [(Item.Key, Item)]
fileprivate init(items: [(Item.Key, Item)]) {
self.items = items
}
}
private final class ItemState {
let peer: Peer
init(peer: Peer) {
self.peer = peer
}
}
private var peers: [Peer] = []
private var itemStates: [PeerId: ItemState] = [:]
public init() {
}
public func update(peers: [Peer], animated: Bool) -> Content {
for peer in peers {
}
var items: [(Content.Item.Key, Content.Item)] = []
for peer in peers {
items.append((Content.Item.Key(peerId: peer.id), Content.Item(peer: peer)))
}
return Content(items: items)
}
}
private let avatarFont = avatarPlaceholderFont(size: 12.0)
private final class ContentNode: ASDisplayNode {
private let unclippedNode: ASImageNode
private let clippedNode: ASImageNode
private var disposable: Disposable?
init(context: AccountContext, peer: Peer, synchronousLoad: Bool) {
self.unclippedNode = ASImageNode()
self.clippedNode = ASImageNode()
super.init()
self.addSubnode(self.unclippedNode)
self.addSubnode(self.clippedNode)
if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: 30.0, height: 30.0), synchronousLoad: synchronousLoad) {
let image = generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.lightGray.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})!
self.updateImage(image: image)
let disposable = (signal
|> deliverOnMainQueue).start(next: { [weak self] imageVersions in
guard let strongSelf = self else {
return
}
let image = imageVersions?.0
if let image = image {
strongSelf.updateImage(image: image)
}
})
self.disposable = disposable
} else {
let image = generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
drawPeerAvatarLetters(context: context, size: size, font: avatarFont, letters: peer.displayLetters, peerId: peer.id)
})!
self.updateImage(image: image)
}
}
private func updateImage(image: UIImage) {
self.unclippedNode.image = image
self.clippedNode.image = generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.5, dy: -1.5).offsetBy(dx: -20.0, dy: 0.0))
})
}
deinit {
self.disposable?.dispose()
}
func updateLayout(size: CGSize, isClipped: Bool, animated: Bool) {
self.unclippedNode.frame = CGRect(origin: CGPoint(), size: size)
self.clippedNode.frame = CGRect(origin: CGPoint(), size: size)
if animated && self.unclippedNode.alpha.isZero != self.clippedNode.alpha.isZero {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: self.unclippedNode, alpha: isClipped ? 0.0 : 1.0)
transition.updateAlpha(node: self.clippedNode, alpha: isClipped ? 1.0 : 0.0)
} else {
self.unclippedNode.alpha = isClipped ? 0.0 : 1.0
self.clippedNode.alpha = isClipped ? 1.0 : 0.0
}
}
}
public final class AnimatedAvatarSetNode: ASDisplayNode {
private var contentNodes: [AnimatedAvatarSetContext.Content.Item.Key: ContentNode] = [:]
override public init() {
super.init()
}
public func update(context: AccountContext, content: AnimatedAvatarSetContext.Content, animated: Bool, synchronousLoad: Bool) -> CGSize {
let itemSize = CGSize(width: 30.0, height: 30.0)
var contentWidth: CGFloat = 0.0
let contentHeight: CGFloat = itemSize.height
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
var validKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
var index = 0
for (key, item) in content.items {
validKeys.append(key)
let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: itemSize)
let itemNode: ContentNode
if let current = self.contentNodes[key] {
itemNode = current
itemNode.updateLayout(size: itemSize, isClipped: index != 0, animated: animated)
transition.updateFrame(node: itemNode, frame: itemFrame)
} else {
itemNode = ContentNode(context: context, peer: item.peer, synchronousLoad: synchronousLoad)
self.addSubnode(itemNode)
self.contentNodes[key] = itemNode
itemNode.updateLayout(size: itemSize, isClipped: index != 0, animated: false)
itemNode.frame = itemFrame
if animated {
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
}
}
contentWidth += itemSize.width - 10.0
index += 1
}
var removeKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
for key in self.contentNodes.keys {
if !validKeys.contains(key) {
removeKeys.append(key)
}
}
for key in removeKeys {
guard let itemNode = self.contentNodes.removeValue(forKey: key) else {
continue
}
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemNode] _ in
itemNode?.removeFromSupernode()
})
itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
}
return CGSize(width: contentWidth, height: contentHeight)
}
}

View File

@ -0,0 +1,16 @@
load("//Config:buck_rule_macros.bzl", "static_library")
static_library(
name = "AnimatedCountLabelNode",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/Display:Display#shared",
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
],
)

View File

@ -0,0 +1,16 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimatedCountLabelNode",
module_name = "AnimatedCountLabelNode",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/Display:Display",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,207 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
public class AnimatedCountLabelNode: ASDisplayNode {
public struct Layout {
public var size: CGSize
public var isTruncated: Bool
}
public enum Segment: Equatable {
public enum Key: Hashable {
case number
case text(Int)
}
case number(Int, NSAttributedString)
case text(Int, NSAttributedString)
public static func ==(lhs: Segment, rhs: Segment) -> Bool {
switch lhs {
case let .number(number, text):
if case let .number(rhsNumber, rhsText) = rhs, number == rhsNumber, text.isEqual(to: rhsText) {
return true
} else {
return false
}
case let .text(index, text):
if case let .text(rhsIndex, rhsText) = rhs, index == rhsIndex, text.isEqual(to: rhsText) {
return true
} else {
return false
}
}
}
public var attributedText: NSAttributedString {
switch self {
case let .number(_, text):
return text
case let .text(_, text):
return text
}
}
var key: Key {
switch self {
case .number:
return .number
case let .text(index, _):
return .text(index)
}
}
}
private var segments: [Segment.Key: (Segment, TextNode)] = [:]
override public init() {
super.init()
}
public func asyncLayout() -> (CGSize, [Segment]) -> (Layout, (Bool) -> Void) {
var segmentLayouts: [Segment.Key: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = [:]
for (segmentKey, segmentAndTextNode) in self.segments {
segmentLayouts[segmentKey] = TextNode.asyncLayout(segmentAndTextNode.1)
}
return { [weak self] size, segments in
for segment in segments {
if segmentLayouts[segment.key] == nil {
segmentLayouts[segment.key] = TextNode.asyncLayout(nil)
}
}
var contentSize = CGSize()
var remainingSize = size
var calculatedSegments: [Segment.Key: (TextNodeLayout, () -> TextNode)] = [:]
var isTruncated = false
var validKeys: [Segment.Key] = []
for segment in segments {
validKeys.append(segment.key)
let (layout, apply) = segmentLayouts[segment.key]!(TextNodeLayoutArguments(attributedString: segment.attributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: remainingSize, alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets(), lineColor: nil, textShadowColor: nil, textStroke: nil))
calculatedSegments[segment.key] = (layout, apply)
contentSize.width += layout.size.width
contentSize.height = max(contentSize.height, layout.size.height)
remainingSize.width = max(0.0, remainingSize.width - layout.size.width)
if layout.truncated {
isTruncated = true
}
}
return (Layout(size: contentSize, isTruncated: isTruncated), { animated in
guard let strongSelf = self else {
return
}
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
var currentOffset = CGPoint()
for segment in segments {
var animation: (CGFloat, Double)?
if let (currentSegment, currentTextNode) = strongSelf.segments[segment.key] {
if case let .number(currentValue, _) = currentSegment, case let .number(updatedValue, _) = segment, animated, currentValue != updatedValue, let snapshot = currentTextNode.layer.snapshotContentTree() {
let offsetY: CGFloat
if currentValue > updatedValue {
offsetY = -floor(currentTextNode.bounds.height * 0.6)
} else {
offsetY = floor(currentTextNode.bounds.height * 0.6)
}
animation = (-offsetY, 0.2)
snapshot.frame = currentTextNode.frame
strongSelf.layer.addSublayer(snapshot)
snapshot.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offsetY), duration: 0.2, removeOnCompletion: false, additive: true)
snapshot.animateScale(from: 1.0, to: 0.3, duration: 0.2, removeOnCompletion: false)
snapshot.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshot] _ in
snapshot?.removeFromSuperlayer()
})
}
}
let (layout, apply) = calculatedSegments[segment.key]!
let textNode = apply()
let textFrame = CGRect(origin: currentOffset, size: layout.size)
if textNode.frame.isEmpty {
textNode.frame = textFrame
if animated, animation == nil {
textNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {
transition.updateFrameAdditive(node: textNode, frame: textFrame)
}
currentOffset.x += layout.size.width
if let (_, currentTextNode) = strongSelf.segments[segment.key] {
if currentTextNode !== textNode {
currentTextNode.removeFromSupernode()
strongSelf.addSubnode(textNode)
}
} else {
strongSelf.addSubnode(textNode)
textNode.displaysAsynchronously = false
textNode.isUserInteractionEnabled = false
}
if let (offset, duration) = animation {
textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, additive: true)
textNode.layer.animateScale(from: 0.3, to: 1.0, duration: duration)
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
}
strongSelf.segments[segment.key] = (segment, textNode)
}
var removeKeys: [Segment.Key] = []
for key in strongSelf.segments.keys {
if !validKeys.contains(key) {
removeKeys.append(key)
}
}
for key in removeKeys {
guard let (_, textNode) = strongSelf.segments.removeValue(forKey: key) else {
continue
}
if animated {
textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textNode] _ in
textNode?.removeFromSupernode()
})
} else {
textNode.removeFromSupernode()
}
}
})
}
}
}
public final class ImmediateAnimatedCountLabelNode: AnimatedCountLabelNode {
public var segments: [AnimatedCountLabelNode.Segment] = []
private var constrainedSize: CGSize?
public func updateLayout(size: CGSize, animated: Bool) -> CGSize {
self.constrainedSize = size
let makeLayout = self.asyncLayout()
let (layout, apply) = makeLayout(size, self.segments)
let _ = apply(animated)
return layout.size
}
public func makeCopy() -> ASDisplayNode {
let node = ImmediateAnimatedCountLabelNode()
node.segments = self.segments
if let constrainedSize = self.constrainedSize {
let _ = node.updateLayout(size: constrainedSize, animated: false)
}
return node
}
}

View File

@ -2290,20 +2290,20 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
var topUpperHistoryBlockMessages: [PeerIdAndMessageNamespace: MessageId.Id] = [:]
final class MessageThreadStatsRecord {
var count: Int = 0
var removedCount: Int = 0
var peers: [ReplyThreadUserMessage] = []
}
var messageThreadStatsDifferences: [MessageId: MessageThreadStatsRecord] = [:]
func addMessageThreadStatsDifference(threadMessageId: MessageId, add: Int, remove: Int, addedMessagePeer: PeerId?, addedMessageId: MessageId?, isOutgoing: Bool) {
func addMessageThreadStatsDifference(threadMessageId: MessageId, remove: Int, addedMessagePeer: PeerId?, addedMessageId: MessageId?, isOutgoing: Bool) {
if let value = messageThreadStatsDifferences[threadMessageId] {
value.count += add - remove
value.removedCount += remove
if let addedMessagePeer = addedMessagePeer, let addedMessageId = addedMessageId {
value.peers.append(ReplyThreadUserMessage(id: addedMessagePeer, messageId: addedMessageId, isOutgoing: isOutgoing))
}
} else {
let value = MessageThreadStatsRecord()
messageThreadStatsDifferences[threadMessageId] = value
value.count = add - remove
value.removedCount = remove
if let addedMessagePeer = addedMessagePeer, let addedMessageId = addedMessageId {
value.peers.append(ReplyThreadUserMessage(id: addedMessagePeer, messageId: addedMessageId, isOutgoing: isOutgoing))
}
@ -2320,7 +2320,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
let messageThreadId = makeThreadIdMessageId(peerId: message.id.peerId, threadId: threadId)
if id.peerId.namespace == Namespaces.Peer.CloudChannel {
if !transaction.messageExists(id: id) {
addMessageThreadStatsDifference(threadMessageId: messageThreadId, add: 1, remove: 0, addedMessagePeer: message.authorId, addedMessageId: id, isOutgoing: !message.flags.contains(.Incoming))
addMessageThreadStatsDifference(threadMessageId: messageThreadId, remove: 0, addedMessagePeer: message.authorId, addedMessageId: id, isOutgoing: !message.flags.contains(.Incoming))
}
}
}
@ -2442,7 +2442,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
}
case let .DeleteMessages(ids):
deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: ids, manualAddMessageThreadStatsDifference: { id, add, remove in
addMessageThreadStatsDifference(threadMessageId: id, add: add, remove: remove, addedMessagePeer: nil, addedMessageId: nil, isOutgoing: false)
addMessageThreadStatsDifference(threadMessageId: id, remove: remove, addedMessagePeer: nil, addedMessageId: nil, isOutgoing: false)
})
case let .UpdateMinAvailableMessage(id):
if let message = transaction.getMessage(id) {
@ -3003,7 +3003,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
// }
for (threadMessageId, difference) in messageThreadStatsDifferences {
updateMessageThreadStats(transaction: transaction, threadMessageId: threadMessageId, difference: difference.count, addedMessagePeers: difference.peers)
updateMessageThreadStats(transaction: transaction, threadMessageId: threadMessageId, removedCount: difference.removedCount, addedMessagePeers: difference.peers)
}
if !peerActivityTimestamps.isEmpty {

View File

@ -234,7 +234,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
if let threadId = updatedMessage.threadId {
let messageThreadId = makeThreadIdMessageId(peerId: updatedMessage.id.peerId, threadId: threadId)
if let authorId = updatedMessage.authorId {
updateMessageThreadStats(transaction: transaction, threadMessageId: messageThreadId, difference: 1, addedMessagePeers: [ReplyThreadUserMessage(id: authorId, messageId: updatedId, isOutgoing: true)])
updateMessageThreadStats(transaction: transaction, threadMessageId: messageThreadId, removedCount: 0, addedMessagePeers: [ReplyThreadUserMessage(id: authorId, messageId: updatedId, isOutgoing: true)])
}
}
}

View File

@ -46,7 +46,7 @@ public func deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [M
if let manualAddMessageThreadStatsDifference = manualAddMessageThreadStatsDifference {
manualAddMessageThreadStatsDifference(messageThreadId, 0, 1)
} else {
updateMessageThreadStats(transaction: transaction, threadMessageId: messageThreadId, difference: -1, addedMessagePeers: [])
updateMessageThreadStats(transaction: transaction, threadMessageId: messageThreadId, removedCount: 1, addedMessagePeers: [])
}
}
}

View File

@ -374,20 +374,27 @@ public class ReplyThreadHistoryContext {
}
public struct ChatReplyThreadMessage: Equatable {
public enum Anchor {
case automatic
case lowerBound
}
public var messageId: MessageId
public var isChannelPost: Bool
public var maxMessage: MessageId?
public var maxReadIncomingMessageId: MessageId?
public var maxReadOutgoingMessageId: MessageId?
public var initialFilledHoles: IndexSet
public var initialAnchor: Anchor
fileprivate init(messageId: MessageId, isChannelPost: Bool, maxMessage: MessageId?, maxReadIncomingMessageId: MessageId?, maxReadOutgoingMessageId: MessageId?, initialFilledHoles: IndexSet) {
fileprivate init(messageId: MessageId, isChannelPost: Bool, maxMessage: MessageId?, maxReadIncomingMessageId: MessageId?, maxReadOutgoingMessageId: MessageId?, initialFilledHoles: IndexSet, initialAnchor: Anchor) {
self.messageId = messageId
self.isChannelPost = isChannelPost
self.maxMessage = maxMessage
self.maxReadIncomingMessageId = maxReadIncomingMessageId
self.maxReadOutgoingMessageId = maxReadOutgoingMessageId
self.initialFilledHoles = initialFilledHoles
self.initialAnchor = initialAnchor
}
}
@ -521,12 +528,18 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
let discussionMessage = Promise<DiscussionMessage?>()
discussionMessage.set(discussionMessageSignal)
let preloadedHistoryPosition: Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, MessageId?, MessageId?), FetchChannelReplyThreadMessageError> = replyInfo.get()
enum Anchor {
case message(MessageId)
case lowerBound
case upperBound
}
let preloadedHistoryPosition: Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> = replyInfo.get()
|> take(1)
|> castError(FetchChannelReplyThreadMessageError.self)
|> mapToSignal { replyInfo -> Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, MessageId?, MessageId?), FetchChannelReplyThreadMessageError> in
|> mapToSignal { replyInfo -> Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> in
if let replyInfo = replyInfo {
return account.postbox.transaction { transaction -> (FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, MessageId?, MessageId?) in
return account.postbox.transaction { transaction -> (FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?) in
var threadInput: FetchMessageHistoryHoleThreadInput = .threadFromChannel(channelMessageId: messageId)
var threadMessageId: MessageId?
transaction.scanMessageAttributes(peerId: replyInfo.commentsPeerId, namespace: Namespaces.Message.Cloud, limit: 1000, { id, attributes in
@ -541,31 +554,47 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
}
return true
})
return (threadInput, replyInfo.commentsPeerId, threadMessageId, atMessageId ?? replyInfo.maxReadIncomingMessageId, replyInfo.maxMessageId)
let anchor: Anchor
if let atMessageId = atMessageId {
anchor = .message(atMessageId)
} else if let maxReadIncomingMessageId = replyInfo.maxReadIncomingMessageId {
anchor = .message(maxReadIncomingMessageId)
} else {
anchor = .lowerBound
}
return (threadInput, replyInfo.commentsPeerId, threadMessageId, anchor, replyInfo.maxMessageId)
}
|> castError(FetchChannelReplyThreadMessageError.self)
} else {
return discussionMessage.get()
|> take(1)
|> castError(FetchChannelReplyThreadMessageError.self)
|> mapToSignal { discussionMessage -> Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, MessageId?, MessageId?), FetchChannelReplyThreadMessageError> in
|> mapToSignal { discussionMessage -> Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> in
guard let discussionMessage = discussionMessage else {
return .fail(.generic)
}
let topMessageId = discussionMessage.messageId
let commentsPeerId = topMessageId.peerId
return .single((.direct(peerId: commentsPeerId, threadId: makeMessageThreadId(topMessageId)), commentsPeerId, discussionMessage.messageId, atMessageId ?? discussionMessage.maxReadIncomingMessageId, discussionMessage.maxMessage))
let anchor: Anchor
if let atMessageId = atMessageId {
anchor = .message(atMessageId)
} else if let maxReadIncomingMessageId = discussionMessage.maxReadIncomingMessageId {
anchor = .message(maxReadIncomingMessageId)
} else {
anchor = .lowerBound
}
return .single((.direct(peerId: commentsPeerId, threadId: makeMessageThreadId(topMessageId)), commentsPeerId, discussionMessage.messageId, anchor, discussionMessage.maxMessage))
}
}
}
let preloadedHistory = preloadedHistoryPosition
|> mapToSignal { peerInput, commentsPeerId, threadMessageId, aroundMessageId, maxMessageId -> Signal<FetchMessageHistoryHoleResult, FetchChannelReplyThreadMessageError> in
|> mapToSignal { peerInput, commentsPeerId, threadMessageId, anchor, maxMessageId -> Signal<(FetchMessageHistoryHoleResult, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in
guard let maxMessageId = maxMessageId else {
return .single(FetchMessageHistoryHoleResult(removedIndices: IndexSet(integersIn: 1 ..< Int(Int32.max - 1)), strictRemovedIndices: IndexSet()))
return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(integersIn: 1 ..< Int(Int32.max - 1)), strictRemovedIndices: IndexSet()), .automatic))
}
return account.postbox.transaction { transaction -> Signal<FetchMessageHistoryHoleResult, FetchChannelReplyThreadMessageError> in
return account.postbox.transaction { transaction -> Signal<(FetchMessageHistoryHoleResult, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in
if let threadMessageId = threadMessageId {
var holes = transaction.getThreadIndexHoles(peerId: threadMessageId.peerId, threadId: makeMessageThreadId(threadMessageId), namespace: Namespaces.Message.Cloud)
holes.remove(integersIn: Int(maxMessageId.id + 1) ..< Int(Int32.max))
@ -576,11 +605,18 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
holes.formIntersection(historyHoles)
}
let anchor: HistoryViewInputAnchor
if let aroundMessageId = aroundMessageId {
anchor = .message(aroundMessageId)
} else {
anchor = .upperBound
let inputAnchor: HistoryViewInputAnchor
let initialAnchor: ChatReplyThreadMessage.Anchor
switch anchor {
case .lowerBound:
inputAnchor = .lowerBound
initialAnchor = .lowerBound
case .upperBound:
inputAnchor = .upperBound
initialAnchor = .automatic
case let .message(id):
inputAnchor = .message(id)
initialAnchor = .automatic
}
let testView = transaction.getMessagesHistoryViewState(
@ -593,24 +629,34 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
Namespaces.Message.Cloud: holes
]
)),
count: 30,
count: 40,
clipHoles: true,
anchor: anchor,
anchor: inputAnchor,
namespaces: .not(Namespaces.Message.allScheduled)
)
if !testView.isLoading {
return .single(FetchMessageHistoryHoleResult(removedIndices: IndexSet(), strictRemovedIndices: IndexSet()))
return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(), strictRemovedIndices: IndexSet()), initialAnchor))
}
}
let direction: MessageHistoryViewRelativeHoleDirection
if let aroundMessageId = aroundMessageId {
direction = .aroundId(aroundMessageId)
} else {
let initialAnchor: ChatReplyThreadMessage.Anchor
switch anchor {
case .lowerBound:
direction = .range(start: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: 1), end: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: Int32.max - 1))
initialAnchor = .lowerBound
case .upperBound:
direction = .range(start: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: Int32.max - 1), end: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: 1))
initialAnchor = .automatic
case let .message(id):
direction = .aroundId(id)
initialAnchor = .automatic
}
return fetchMessageHistoryHole(accountPeerId: account.peerId, source: .network(account.network), postbox: account.postbox, peerInput: peerInput, namespace: Namespaces.Message.Cloud, direction: direction, space: .everywhere, count: 30)
return fetchMessageHistoryHole(accountPeerId: account.peerId, source: .network(account.network), postbox: account.postbox, peerInput: peerInput, namespace: Namespaces.Message.Cloud, direction: direction, space: .everywhere, count: 40)
|> castError(FetchChannelReplyThreadMessageError.self)
|> map { result -> (FetchMessageHistoryHoleResult, ChatReplyThreadMessage.Anchor) in
return (result, initialAnchor)
}
}
|> castError(FetchChannelReplyThreadMessageError.self)
|> switchToLatest
@ -622,10 +668,11 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
|> castError(FetchChannelReplyThreadMessageError.self),
preloadedHistory
)
|> mapToSignal { discussionMessage, initialFilledHoles -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
|> mapToSignal { discussionMessage, initialFilledHolesAndInitialAnchor -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
guard let discussionMessage = discussionMessage else {
return .fail(.generic)
}
let (initialFilledHoles, initialAnchor) = initialFilledHolesAndInitialAnchor
return account.postbox.transaction { transaction -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
for range in initialFilledHoles.strictRemovedIndices.rangeView {
transaction.removeThreadIndexHole(peerId: discussionMessage.messageId.peerId, threadId: makeMessageThreadId(discussionMessage.messageId), namespace: Namespaces.Message.Cloud, space: .everywhere, range: Int32(range.lowerBound) ... Int32(range.upperBound))
@ -637,7 +684,8 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
maxMessage: discussionMessage.maxMessage,
maxReadIncomingMessageId: discussionMessage.maxReadIncomingMessageId,
maxReadOutgoingMessageId: discussionMessage.maxReadOutgoingMessageId,
initialFilledHoles: initialFilledHoles.removedIndices
initialFilledHoles: initialFilledHoles.removedIndices,
initialAnchor: initialAnchor
))
}
|> castError(FetchChannelReplyThreadMessageError.self)

View File

@ -35,11 +35,11 @@ struct ReplyThreadUserMessage {
var isOutgoing: Bool
}
func updateMessageThreadStats(transaction: Transaction, threadMessageId: MessageId, difference: Int, addedMessagePeers: [ReplyThreadUserMessage]) {
updateMessageThreadStatsInternal(transaction: transaction, threadMessageId: threadMessageId, difference: difference, addedMessagePeers: addedMessagePeers, allowChannel: false)
func updateMessageThreadStats(transaction: Transaction, threadMessageId: MessageId, removedCount: Int, addedMessagePeers: [ReplyThreadUserMessage]) {
updateMessageThreadStatsInternal(transaction: transaction, threadMessageId: threadMessageId, removedCount: removedCount, addedMessagePeers: addedMessagePeers, allowChannel: false)
}
private func updateMessageThreadStatsInternal(transaction: Transaction, threadMessageId: MessageId, difference: Int, addedMessagePeers: [ReplyThreadUserMessage], allowChannel: Bool) {
private func updateMessageThreadStatsInternal(transaction: Transaction, threadMessageId: MessageId, removedCount: Int, addedMessagePeers: [ReplyThreadUserMessage], allowChannel: Bool) {
guard let channel = transaction.getPeer(threadMessageId.peerId) as? TelegramChannel else {
return
}
@ -77,12 +77,21 @@ private func updateMessageThreadStatsInternal(transaction: Transaction, threadMe
}
transaction.updateMessage(threadMessageId, update: { currentMessage in
let countDifference = Int32(difference)
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReplyThreadMessageAttribute {
let count = max(0, attribute.count + countDifference)
var countDifference = -removedCount
for addedMessage in addedMessagePeers {
if let maxMessageId = attribute.maxMessageId {
if addedMessage.messageId.id > maxMessageId {
countDifference += 1
}
} else {
countDifference += 1
}
}
let count = max(0, attribute.count + Int32(countDifference))
var maxMessageId = attribute.maxMessageId
var maxReadMessageId = attribute.maxReadMessageId
if let maxAddedId = addedMessagePeers.map({ $0.messageId.id }).max() {
@ -109,6 +118,6 @@ private func updateMessageThreadStatsInternal(transaction: Transaction, threadMe
})
if let channelThreadMessageId = channelThreadMessageId {
updateMessageThreadStatsInternal(transaction: transaction, threadMessageId: channelThreadMessageId, difference: difference, addedMessagePeers: addedMessagePeers, allowChannel: true)
updateMessageThreadStatsInternal(transaction: transaction, threadMessageId: channelThreadMessageId, removedCount: removedCount, addedMessagePeers: addedMessagePeers, allowChannel: true)
}
}

View File

@ -186,8 +186,6 @@ framework(
"//submodules/MessageReactionListUI:MessageReactionListUI",
"//submodules/SegmentedControlNode:SegmentedControlNode",
"//submodules/AppBundle:AppBundle",
#"//submodules/WalletUI:WalletUI",
#"//submodules/WalletCore:WalletCore",
"//submodules/Markdown:Markdown",
"//submodules/SearchPeerMembers:SearchPeerMembers",
"//submodules/WidgetItems:WidgetItems",
@ -211,6 +209,8 @@ framework(
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
"//submodules/GalleryData:GalleryData",
"//submodules/ChatInterfaceState:ChatInterfaceState",
"//submodules/AnimatedCountLabelNode:AnimatedCountLabelNode",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -206,6 +206,8 @@ swift_library(
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
"//submodules/GalleryData:GalleryData",
"//submodules/ChatInterfaceState:ChatInterfaceState",
"//submodules/AnimatedCountLabelNode:AnimatedCountLabelNode",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
],
visibility = [
"//visibility:public",

View File

@ -178,6 +178,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var didSetChatLocationInfoReady = false
private let chatLocationInfoData: ChatLocationInfoData
private let cachedDataReady = Promise<Bool>()
private var didSetCachedDataReady = false
private var presentationInterfaceState: ChatPresentationInterfaceState
private var chatTitleView: ChatTitleView?
@ -289,6 +292,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var checkedPeerChatServiceActions = false
private var willAppear = false
private var didAppear = false
private var scheduledActivateInput = false
@ -2186,12 +2190,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: items, reactionItems: [], gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
})
}, openMessageReplies: { [weak self] messageId, isChannelPost in
}, openMessageReplies: { [weak self] messageId, isChannelPost, displayModalProgress in
guard let strongSelf = self else {
return
}
strongSelf.openMessageReplies(messageId: messageId, isChannelPost: isChannelPost, atMessage: nil)
strongSelf.openMessageReplies(messageId: messageId, isChannelPost: isChannelPost, atMessage: nil, displayModalProgress: displayModalProgress)
}, openReplyThreadOriginalMessage: { [weak self] message in
guard let strongSelf = self else {
return
@ -2206,8 +2210,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
for attribute in message.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
if let threadMessageId = threadMessageId {
if let navigationController = strongSelf.navigationController as? NavigationController {
strongSelf.openMessageReplies(messageId: threadMessageId, isChannelPost: true, atMessage: attribute.messageId)
if let _ = strongSelf.navigationController as? NavigationController {
strongSelf.openMessageReplies(messageId: threadMessageId, isChannelPost: true, atMessage: attribute.messageId, displayModalProgress: true)
}
} else {
strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId))
@ -2673,14 +2677,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
let text: String
if count == 0 {
text = strongSelf.presentationData.strings.Conversation_TitleNoComments
} else {
text = strongSelf.presentationData.strings.Conversation_TitleComments(Int32(count))
}
strongSelf.chatTitleView?.titleContent = .replyThread(type: replyThreadType, text: text)
strongSelf.chatTitleView?.titleContent = .replyThread(type: replyThreadType, count: count)
let firstTime = strongSelf.peerView == nil
strongSelf.peerView = peerView
@ -3264,8 +3261,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if let _ = combinedInitialData.cachedData as? CachedSecretChatData {
}
if case .replyThread = strongSelf.chatLocation {
pinnedMessageId = nil
if case let .replyThread(replyThreadMessageId) = strongSelf.chatLocation {
pinnedMessageId = replyThreadMessageId.messageId
}
var pinnedMessage: Message?
@ -3435,7 +3432,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let callsDataUpdated = strongSelf.presentationInterfaceState.callsAvailable != callsAvailable || strongSelf.presentationInterfaceState.callsPrivate != callsPrivate
if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.pinnedMessage?.stableVersion != pinnedMessage?.stableVersion || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || pinnedMessageUpdated || callsDataUpdated || strongSelf.presentationInterfaceState.slowmodeState != slowmodeState {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in
return state
.updatedPinnedMessageId(pinnedMessageId)
.updatedPinnedMessage(pinnedMessage)
@ -3478,6 +3475,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
.updatedSlowmodeState(slowmodeState)
})
}
if !strongSelf.didSetCachedDataReady {
strongSelf.didSetCachedDataReady = true
strongSelf.cachedDataReady.set(.single(true))
}
}
})
@ -3489,8 +3491,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyState.get(), self._chatLocationInfoReady.get(), initialData) |> map { _, chatLocationInfoReady, _ in
return chatLocationInfoReady
self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyState.get(), self._chatLocationInfoReady.get(), self.cachedDataReady.get(), initialData) |> map { _, chatLocationInfoReady, cachedDataReady, _ in
return chatLocationInfoReady && cachedDataReady
})
if self.context.sharedContext.immediateExperimentalUISettings.crashOnLongQueries {
@ -5396,6 +5398,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.willAppear = true
if self.scheduledActivateInput {
self.scheduledActivateInput = false
@ -8318,12 +8322,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
private func openMessageReplies(messageId: MessageId, isChannelPost: Bool, atMessage atMessageId: MessageId?) {
private func openMessageReplies(messageId: MessageId, isChannelPost: Bool, atMessage atMessageId: MessageId?, displayModalProgress: Bool) {
guard let navigationController = self.navigationController as? NavigationController else {
return
}
if self.controllerInteraction?.currentMessageWithLoadingReplyThread == messageId {
if !displayModalProgress, self.controllerInteraction?.currentMessageWithLoadingReplyThread == messageId {
return
}
@ -8332,7 +8336,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return EmptyDisposable
}
if controllerInteraction.currentMessageWithLoadingReplyThread != messageId {
if !displayModalProgress, controllerInteraction.currentMessageWithLoadingReplyThread != messageId {
let previousId = controllerInteraction.currentMessageWithLoadingReplyThread
controllerInteraction.currentMessageWithLoadingReplyThread = messageId
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(messageId)
@ -8346,7 +8350,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
return
}
if controllerInteraction.currentMessageWithLoadingReplyThread == messageId {
if !displayModalProgress, controllerInteraction.currentMessageWithLoadingReplyThread == messageId {
controllerInteraction.currentMessageWithLoadingReplyThread = nil
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(messageId)
}
@ -8360,7 +8364,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.navigationActionDisposable.set((ChatControllerImpl.openMessageReplies(context: self.context, navigationController: navigationController, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, messageId: messageId, isChannelPost: isChannelPost, atMessage: atMessageId, displayModalProgress: false)
}, messageId: messageId, isChannelPost: isChannelPost, atMessage: atMessageId, displayModalProgress: displayModalProgress)
|> afterDisposed {
progress.dispose()
}).start())
@ -8390,6 +8394,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let subject: ChatControllerSubject?
if let atMessageId = atMessageId {
subject = .message(atMessageId)
} else if result.scrollToLowerBound {
subject = .message(MessageId(peerId: result.message.messageId.peerId, namespace: Namespaces.Message.Cloud, id: 1))
} else {
subject = nil
}

View File

@ -112,7 +112,7 @@ public final class ChatControllerInteraction {
let animateDiceSuccess: () -> Void
let greetingStickerNode: () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, () -> Void)?
let openPeerContextMenu: (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void
let openMessageReplies: (MessageId, Bool) -> Void
let openMessageReplies: (MessageId, Bool, Bool) -> Void
let openReplyThreadOriginalMessage: (Message) -> Void
let requestMessageUpdate: (MessageId) -> Void
@ -197,7 +197,7 @@ public final class ChatControllerInteraction {
animateDiceSuccess: @escaping () -> Void,
greetingStickerNode: @escaping () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, () -> Void)?,
openPeerContextMenu: @escaping (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void,
openMessageReplies: @escaping (MessageId, Bool) -> Void,
openMessageReplies: @escaping (MessageId, Bool, Bool) -> Void,
openReplyThreadOriginalMessage: @escaping (Message) -> Void,
requestMessageUpdate: @escaping (MessageId) -> Void,
cancelInteractiveKeyboardGestures: @escaping () -> Void,
@ -319,7 +319,7 @@ public final class ChatControllerInteraction {
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {

View File

@ -1100,6 +1100,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
private func processDisplayedItemRangeChanged(displayedRange: ListViewDisplayedItemRange, transactionState: ChatHistoryTransactionOpaqueState) {
let historyView = transactionState.historyView
var isTopReplyThreadMessageShownValue = false
if let visible = displayedRange.visibleRange {
let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)
if indexRange.0 > indexRange.1 {
@ -1120,8 +1121,6 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
var messagesWithPreloadableMediaToEarlier: [(Message, Media)] = []
var messagesWithPreloadableMediaToLater: [(Message, Media)] = []
var isTopReplyThreadMessageShownValue = false
if indexRange.0 <= indexRange.1 {
for i in (indexRange.0 ... indexRange.1) {
switch historyView.filteredEntries[i] {
@ -1320,9 +1319,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.maxVisibleMessageIndexUpdated?(maxOverallIndex)
}
}
self.isTopReplyThreadMessageShown.set(isTopReplyThreadMessageShownValue)
}
self.isTopReplyThreadMessageShown.set(isTopReplyThreadMessageShownValue)
if let loaded = displayedRange.loadedRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last {
if loaded.firstIndex < 5 && historyView.originalView.laterId != nil {

View File

@ -85,41 +85,52 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, context: A
}
var scrollPosition: ChatHistoryViewScrollPosition?
if let maxReadIndex = view.maxReadIndex, tagMask == nil, view.isAddedToChatList {
let canScrollToRead: Bool
if case .replyThread = chatLocation {
canScrollToRead = true
} else if view.isAddedToChatList {
canScrollToRead = true
} else {
canScrollToRead = false
}
if let maxReadIndex = view.maxReadIndex, tagMask == nil, canScrollToRead {
let aroundIndex = maxReadIndex
scrollPosition = .unread(index: maxReadIndex)
var targetIndex = 0
for i in 0 ..< view.entries.count {
if view.entries[i].index >= aroundIndex {
targetIndex = i
break
if case .peer = chatLocation {
var targetIndex = 0
for i in 0 ..< view.entries.count {
if view.entries[i].index >= aroundIndex {
targetIndex = i
break
}
}
}
let maxIndex = targetIndex + count / 2
let minIndex = targetIndex - count / 2
if minIndex <= 0 && view.holeEarlier {
fadeIn = true
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
if maxIndex >= targetIndex {
if view.holeLater {
let maxIndex = targetIndex + count / 2
let minIndex = targetIndex - count / 2
if minIndex <= 0 && view.holeEarlier {
fadeIn = true
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
if view.holeEarlier {
var incomingCount: Int32 = 0
inner: for entry in view.entries.reversed() {
if !entry.message.flags.intersection(.IsIncomingMask).isEmpty {
incomingCount += 1
}
}
if case let .peer(peerId) = chatLocation, let combinedReadStates = view.fixedReadStates, case let .peer(readStates) = combinedReadStates, let readState = readStates[peerId], readState.count == incomingCount {
} else {
if maxIndex >= targetIndex {
if view.holeLater {
fadeIn = true
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
if view.holeEarlier {
var incomingCount: Int32 = 0
inner: for entry in view.entries.reversed() {
if !entry.message.flags.intersection(.IsIncomingMask).isEmpty {
incomingCount += 1
}
}
if case let .peer(peerId) = chatLocation, let combinedReadStates = view.fixedReadStates, case let .peer(readStates) = combinedReadStates, let readState = readStates[peerId], readState.count == incomingCount {
} else {
fadeIn = true
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
}
}
}
}
} else if view.isAddedToChatList, let historyScrollState = (initialData?.chatInterfaceState as? ChatInterfaceState)?.historyScrollState, tagMask == nil {
@ -294,6 +305,7 @@ struct ReplyThreadInfo {
var message: ChatReplyThreadMessage
var isChannelPost: Bool
var isEmpty: Bool
var scrollToLowerBound: Bool
var contextHolder: Atomic<ChatLocationContextHolder?>
}
@ -305,9 +317,7 @@ enum ReplyThreadSubject {
func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThreadSubject, atMessageId: MessageId?) -> Signal<ReplyThreadInfo, FetchChannelReplyThreadMessageError> {
let message: Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError>
switch subject {
case let .channelPost(messageId):
message = fetchChannelReplyThreadMessage(account: context.account, messageId: messageId, atMessageId: atMessageId)
case let .groupMessage(messageId):
case .channelPost(let messageId), .groupMessage(let messageId):
message = fetchChannelReplyThreadMessage(account: context.account, messageId: messageId, atMessageId: atMessageId)
}
@ -316,16 +326,27 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
let input: ChatHistoryLocationInput
if let atMessageId = atMessageId {
let scrollToLowerBound: Bool
switch replyThreadMessage.initialAnchor {
case .automatic:
if let atMessageId = atMessageId {
input = ChatHistoryLocationInput(
content: .InitialSearch(location: .id(atMessageId), count: 40),
id: 0
)
} else {
input = ChatHistoryLocationInput(
content: .Initial(count: 40),
id: 0
)
}
scrollToLowerBound = false
case .lowerBound:
input = ChatHistoryLocationInput(
content: .InitialSearch(location: .id(atMessageId), count: 30),
id: 0
)
} else {
input = ChatHistoryLocationInput(
content: .Initial(count: 30),
content: .Navigation(index: .lowerBound, anchorIndex: .lowerBound, count: 40),
id: 0
)
scrollToLowerBound = true
}
let preloadSignal = preloadedChatHistoryViewForLocation(
@ -359,6 +380,7 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea
message: replyThreadMessage,
isChannelPost: replyThreadMessage.isChannelPost,
isEmpty: isEmpty,
scrollToLowerBound: scrollToLowerBound,
contextHolder: chatLocationContextHolder
)
}

View File

@ -601,7 +601,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replies"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
c.dismiss(completion: {
controllerInteraction.openMessageReplies(messages[0].id, true)
controllerInteraction.openMessageReplies(messages[0].id, true, true)
})
})))
}

View File

@ -1253,7 +1253,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
for attribute in item.message.attributes {
if let _ = attribute as? ReplyThreadMessageAttribute {
item.controllerInteraction.openMessageReplies(item.message.id, true)
item.controllerInteraction.openMessageReplies(item.message.id, true, false)
return
}
}

View File

@ -8,15 +8,18 @@ import TelegramCore
import SyncCore
import TelegramPresentationData
import RadialStatusNode
import AnimatedCountLabelNode
import AnimatedAvatarSetNode
final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
private let separatorNode: ASDisplayNode
private let textNode: TextNode
private let alternativeTextNode: TextNode
private let countNode: AnimatedCountLabelNode
private let alternativeCountNode: AnimatedCountLabelNode
private let iconNode: ASImageNode
private let arrowNode: ASImageNode
private let buttonNode: HighlightTrackingButtonNode
private let avatarsNode: MergedAvatarsNode
private let avatarsContext: AnimatedAvatarSetContext
private let avatarsNode: AnimatedAvatarSetNode
private let unreadIconNode: ASImageNode
private var statusNode: RadialStatusNode?
@ -24,17 +27,8 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
self.separatorNode = ASDisplayNode()
self.separatorNode.isUserInteractionEnabled = false
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .topLeft
self.textNode.contentsScale = UIScreenScale
self.textNode.displaysAsynchronously = true
self.alternativeTextNode = TextNode()
self.alternativeTextNode.isUserInteractionEnabled = false
self.alternativeTextNode.contentMode = .topLeft
self.alternativeTextNode.contentsScale = UIScreenScale
self.alternativeTextNode.displaysAsynchronously = true
self.countNode = AnimatedCountLabelNode()
self.alternativeCountNode = AnimatedCountLabelNode()
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
@ -51,7 +45,8 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.isUserInteractionEnabled = false
self.avatarsNode = MergedAvatarsNode()
self.avatarsContext = AnimatedAvatarSetContext()
self.avatarsNode = AnimatedAvatarSetNode()
self.avatarsNode.isUserInteractionEnabled = false
self.buttonNode = HighlightTrackingButtonNode()
@ -59,8 +54,8 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
super.init()
self.buttonNode.addSubnode(self.separatorNode)
self.buttonNode.addSubnode(self.textNode)
self.buttonNode.addSubnode(self.alternativeTextNode)
self.buttonNode.addSubnode(self.countNode)
self.buttonNode.addSubnode(self.alternativeCountNode)
self.buttonNode.addSubnode(self.iconNode)
self.buttonNode.addSubnode(self.unreadIconNode)
self.buttonNode.addSubnode(self.arrowNode)
@ -70,12 +65,7 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
let nodes: [ASDisplayNode] = [
strongSelf.textNode,
strongSelf.alternativeTextNode,
strongSelf.iconNode,
strongSelf.avatarsNode,
strongSelf.unreadIconNode,
strongSelf.arrowNode,
strongSelf.buttonNode
]
for node in nodes {
if highlighted {
@ -103,13 +93,13 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
if item.message.id.peerId.isReplies {
item.controllerInteraction.openReplyThreadOriginalMessage(item.message)
} else {
item.controllerInteraction.openMessageReplies(item.message.id, true)
item.controllerInteraction.openMessageReplies(item.message.id, true, false)
}
}
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
let textLayout = TextNode.asyncLayout(self.textNode)
let alternativeTextLayout = TextNode.asyncLayout(self.alternativeTextNode)
let makeCountLayout = self.countNode.asyncLayout()
let makeAlternativeCountLayout = self.alternativeCountNode.asyncLayout()
return { item, layoutConstants, preparePosition, _, constrainedSize in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
@ -146,18 +136,59 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
}
}
let rawText: String
let rawAlternativeText: String
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
let textFont = item.presentationData.messageFont
let rawSegments: [AnimatedCountLabelNode.Segment]
let rawAlternativeSegments: [AnimatedCountLabelNode.Segment]
if item.message.id.peerId.isReplies {
rawText = item.presentationData.strings.Conversation_ViewReply
rawAlternativeText = rawText
rawSegments = [.text(100, NSAttributedString(string: item.presentationData.strings.Conversation_ViewReply, font: textFont, textColor: messageTheme.accentTextColor))]
rawAlternativeSegments = rawSegments
} else if dateReplies > 0 {
rawText = item.presentationData.strings.Conversation_MessageViewComments(Int32(dateReplies))
rawAlternativeText = rawText
var commentsPart = item.presentationData.strings.Conversation_MessageViewComments(Int32(dateReplies))
if let startIndex = commentsPart.firstIndex(of: "["), let endIndex = commentsPart.firstIndex(of: "]") {
commentsPart.removeSubrange(startIndex ... endIndex)
}
var segments: [AnimatedCountLabelNode.Segment] = []
let (rawText, ranges) = item.presentationData.strings.Conversation_MessageViewCommentsFormat("\(dateReplies)", commentsPart)
var textIndex = 0
var latestIndex = 0
for (index, range) in ranges {
var lowerSegmentIndex = range.lowerBound
if index != 0 {
lowerSegmentIndex = min(lowerSegmentIndex, latestIndex)
} else {
if latestIndex < range.lowerBound {
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: latestIndex) ..< rawText.index(rawText.startIndex, offsetBy: range.lowerBound)])
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: messageTheme.accentTextColor)))
textIndex += 1
}
}
latestIndex = range.upperBound
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: lowerSegmentIndex) ..< rawText.index(rawText.startIndex, offsetBy: range.upperBound)])
if index == 0 {
segments.append(.number(dateReplies, NSAttributedString(string: part, font: textFont, textColor: messageTheme.accentTextColor)))
} else {
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: messageTheme.accentTextColor)))
textIndex += 1
}
}
if latestIndex < rawText.count {
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: latestIndex)...])
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: messageTheme.accentTextColor)))
textIndex += 1
}
rawSegments = segments
rawAlternativeSegments = rawSegments
} else {
rawText = item.presentationData.strings.Conversation_MessageLeaveComment
rawAlternativeText = item.presentationData.strings.Conversation_MessageLeaveCommentShort
rawSegments = [.text(100, NSAttributedString(string: item.presentationData.strings.Conversation_MessageLeaveComment, font: textFont, textColor: messageTheme.accentTextColor))]
rawAlternativeSegments = [.text(100, NSAttributedString(string: item.presentationData.strings.Conversation_MessageLeaveCommentShort, font: textFont, textColor: messageTheme.accentTextColor))]
}
let imageSize: CGFloat = 30.0
@ -169,23 +200,16 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
} else {
textLeftInset = 15.0 + imageSize * min(1.0, CGFloat(replyPeers.count)) + (imageSpacing) * max(0.0, min(2.0, CGFloat(replyPeers.count - 1)))
}
let textRightInset: CGFloat = 33.0
let textRightInset: CGFloat = 36.0
let textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - horizontalInset - textLeftInset - textRightInset), height: constrainedSize.height)
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
let textInsets = UIEdgeInsets()//(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0)
let textFont = item.presentationData.messageFont
let (countLayout, countApply) = makeCountLayout(textConstrainedSize, rawSegments)
let (alternativeCountLayout, alternativeCountApply) = makeAlternativeCountLayout(textConstrainedSize, rawAlternativeSegments)
let attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.accentTextColor)
let alternativeAttributedText = NSAttributedString(string: rawAlternativeText, font: textFont, textColor: messageTheme.accentTextColor)
let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0)
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor))
let (alternativeTextLayout, alternativeTextApply) = alternativeTextLayout(TextNodeLayoutArguments(attributedString: alternativeAttributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor))
var textFrame = CGRect(origin: CGPoint(x: -textInsets.left + textLeftInset, y: -textInsets.top + 5.0 + topOffset), size: textLayout.size)
var textFrame = CGRect(origin: CGPoint(x: -textInsets.left + textLeftInset - 2.0, y: -textInsets.top + 5.0 + topOffset), size: countLayout.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: layoutConstants.text.bubbleInsets.top - 5.0 + UIScreenPixel)
@ -218,25 +242,38 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
if let strongSelf = self {
strongSelf.item = item
strongSelf.textNode.displaysAsynchronously = !item.presentationData.isPreview
strongSelf.alternativeTextNode.displaysAsynchronously = !item.presentationData.isPreview
let transition: ContainedViewLayoutTransition
if animation.isAnimated {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
strongSelf.textNode.isHidden = textLayout.truncated
strongSelf.alternativeTextNode.isHidden = !strongSelf.textNode.isHidden
strongSelf.countNode.isHidden = countLayout.isTruncated
strongSelf.alternativeCountNode.isHidden = !strongSelf.countNode.isHidden
let _ = textApply()
let _ = alternativeTextApply()
let _ = countApply(animation.isAnimated)
let _ = alternativeCountApply(animation.isAnimated)
let adjustedTextFrame = textFrame
strongSelf.textNode.frame = adjustedTextFrame
strongSelf.alternativeTextNode.frame = CGRect(origin: adjustedTextFrame.origin, size: alternativeTextLayout.size)
if strongSelf.countNode.frame.isEmpty {
strongSelf.countNode.frame = adjustedTextFrame
} else {
transition.updateFrameAdditive(node: strongSelf.countNode, frame: adjustedTextFrame)
}
if strongSelf.alternativeCountNode.frame.isEmpty {
strongSelf.alternativeCountNode.frame = CGRect(origin: adjustedTextFrame.origin, size: alternativeCountLayout.size)
} else {
transition.updateFrameAdditive(node: strongSelf.alternativeCountNode, frame: CGRect(origin: adjustedTextFrame.origin, size: alternativeCountLayout.size))
}
let effectiveTextFrame: CGRect
if !strongSelf.alternativeTextNode.isHidden {
effectiveTextFrame = strongSelf.alternativeTextNode.frame
if !strongSelf.alternativeCountNode.isHidden {
effectiveTextFrame = strongSelf.alternativeCountNode.frame
} else {
effectiveTextFrame = strongSelf.textNode.frame
effectiveTextFrame = strongSelf.countNode.frame
}
if let iconImage = iconImage {
@ -247,17 +284,31 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
if let arrowImage = arrowImage {
strongSelf.arrowNode.image = arrowImage
let arrowFrame = CGRect(origin: CGPoint(x: boundingWidth - 33.0, y: 6.0 + topOffset), size: arrowImage.size)
strongSelf.arrowNode.frame = arrowFrame
if strongSelf.arrowNode.frame.isEmpty {
strongSelf.arrowNode.frame = arrowFrame
} else {
transition.updateFrameAdditive(node: strongSelf.arrowNode, frame: arrowFrame)
}
if let unreadIconImage = unreadIconImage {
strongSelf.unreadIconNode.image = unreadIconImage
strongSelf.unreadIconNode.frame = CGRect(origin: CGPoint(x: effectiveTextFrame.maxX + 4.0, y: effectiveTextFrame.minY + floor((effectiveTextFrame.height - unreadIconImage.size.height) / 2.0) - 1.0), size: unreadIconImage.size)
let unreadIconFrame = CGRect(origin: CGPoint(x: effectiveTextFrame.maxX + 4.0, y: effectiveTextFrame.minY + floor((effectiveTextFrame.height - unreadIconImage.size.height) / 2.0) + 1.0), size: unreadIconImage.size)
if strongSelf.unreadIconNode.frame.isEmpty {
strongSelf.unreadIconNode.frame = unreadIconFrame
} else {
transition.updateFrameAdditive(node: strongSelf.unreadIconNode, frame: unreadIconFrame)
}
}
}
strongSelf.unreadIconNode.isHidden = !hasUnseenReplies
strongSelf.iconNode.isHidden = !replyPeers.isEmpty
if strongSelf.unreadIconNode.alpha.isZero != !hasUnseenReplies {
transition.updateAlpha(node: strongSelf.unreadIconNode, alpha: hasUnseenReplies ? 1.0 : 0.0)
if hasUnseenReplies {
strongSelf.unreadIconNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0)
}
}
let hasActivity = item.controllerInteraction.currentMessageWithLoadingReplyThread == item.message.id
@ -272,7 +323,14 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
strongSelf.buttonNode.addSubnode(statusNode)
}
let statusSize = CGSize(width: 20.0, height: 20.0)
statusNode.frame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - 11.0, y: 8.0 + topOffset), size: statusSize)
let statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - 11.0, y: 8.0 + topOffset), size: statusSize)
if statusNode.frame.isEmpty {
statusNode.frame = statusFrame
} else {
transition.updateFrameAdditive(node: statusNode, frame: statusFrame)
}
statusNode.transitionToState(.progress(color: messageTheme.accentTextColor, lineWidth: 1.5, value: nil, cancelEnabled: false), animated: false, synchronous: false, completion: {})
} else {
strongSelf.arrowNode.isHidden = false
@ -285,10 +343,24 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
}
}
let avatarsFrame = CGRect(origin: CGPoint(x: 13.0, y: 3.0 + topOffset), size: CGSize(width: imageSize * 3.0, height: imageSize))
let avatarContent = strongSelf.avatarsContext.update(peers: replyPeers, animated: animation.isAnimated)
let avatarsSize = strongSelf.avatarsNode.update(context: item.context, content: avatarContent, animated: animation.isAnimated, synchronousLoad: synchronousLoad)
let iconAlpha: CGFloat = avatarsSize.width.isZero ? 1.0 : 0.0
if iconAlpha.isZero != strongSelf.iconNode.alpha.isZero {
transition.updateAlpha(node: strongSelf.iconNode, alpha: iconAlpha)
if animation.isAnimated {
if iconAlpha.isZero {
} else {
strongSelf.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
}
}
let avatarsFrame = CGRect(origin: CGPoint(x: 13.0, y: 3.0 + topOffset), size: avatarsSize)
strongSelf.avatarsNode.frame = avatarsFrame
strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size)
strongSelf.avatarsNode.update(context: item.context, peers: replyPeers, synchronousLoad: synchronousLoad, imageSize: imageSize, imageSpacing: imageSpacing, borderWidth: 2.0 - UIScreenPixel)
//strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size)
//strongSelf.avatarsNode.update(context: item.context, peers: replyPeers, synchronousLoad: synchronousLoad, imageSize: imageSize, imageSpacing: imageSpacing, borderWidth: 2.0 - UIScreenPixel)
strongSelf.separatorNode.backgroundColor = messageTheme.polls.separator
strongSelf.separatorNode.isHidden = !displaySeparator

View File

@ -748,7 +748,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
for attribute in item.message.attributes {
if let _ = attribute as? ReplyThreadMessageAttribute {
item.controllerInteraction.openMessageReplies(item.message.id, true)
item.controllerInteraction.openMessageReplies(item.message.id, true, false)
return
}
}

View File

@ -814,7 +814,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
for attribute in item.message.attributes {
if let _ = attribute as? ReplyThreadMessageAttribute {
item.controllerInteraction.openMessageReplies(item.message.id, true)
item.controllerInteraction.openMessageReplies(item.message.id, true, false)
return
}
}

View File

@ -451,7 +451,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {

View File

@ -16,16 +16,16 @@ import ChatTitleActivityNode
import LocalizedPeerData
import PhoneNumberFormat
import ChatTitleActivityNode
import AnimatedCountLabelNode
enum ChatTitleContent {
enum ReplyThreadType {
case replies
case comments
case replies
}
case peer(peerView: PeerView, onlineMemberCount: Int32?, isScheduledMessages: Bool)
case replyThread(type: ReplyThreadType, text: String)
case group([Peer])
case replyThread(type: ReplyThreadType, count: Int)
case custom(String)
}
@ -45,7 +45,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
private var nameDisplayOrder: PresentationPersonNameOrder
private let contentContainer: ASDisplayNode
let titleNode: ImmediateTextNode
let titleNode: ImmediateAnimatedCountLabelNode
let titleLeftIconNode: ASImageNode
let titleRightIconNode: ASImageNode
let titleCredibilityIconNode: ASImageNode
@ -100,7 +100,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
if let titleContent = self.titleContent {
let titleTheme = self.hasEmbeddedTitleContent ? defaultDarkPresentationTheme : self.theme
var string: NSAttributedString?
var segments: [AnimatedCountLabelNode.Segment] = []
var titleLeftIcon: ChatTitleIcon = .none
var titleRightIcon: ChatTitleIcon = .none
var titleScamIcon = false
@ -109,24 +109,24 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
case let .peer(peerView, _, isScheduledMessages):
if peerView.peerId.isReplies {
let typeText: String = self.strings.DialogList_Replies
string = NSAttributedString(string: typeText, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
segments = [.text(0, NSAttributedString(string: typeText, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
isEnabled = false
} else if isScheduledMessages {
if peerView.peerId == self.account.peerId {
string = NSAttributedString(string: self.strings.ScheduledMessages_RemindersTitle, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
segments = [.text(0, NSAttributedString(string: self.strings.ScheduledMessages_RemindersTitle, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
} else {
string = NSAttributedString(string: self.strings.ScheduledMessages_Title, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
segments = [.text(0, NSAttributedString(string: self.strings.ScheduledMessages_Title, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
}
isEnabled = false
} else {
if let peer = peerViewMainPeer(peerView) {
if peerView.peerId == self.account.peerId {
string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
segments = [.text(0, NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
} else {
if !peerView.peerIsContact, let user = peer as? TelegramUser, !user.flags.contains(.isSupport), user.botInfo == nil, let phone = user.phone, !phone.isEmpty {
string = NSAttributedString(string: formatPhoneNumber(phone), font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
segments = [.text(0, NSAttributedString(string: formatPhoneNumber(phone), font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
} else {
string = NSAttributedString(string: peer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
segments = [.text(0, NSAttributedString(string: peer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
}
}
titleScamIcon = peer.isScam
@ -140,25 +140,78 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
}
}
}
case let .replyThread(type, text):
let typeText: String
if !text.isEmpty {
typeText = text
case let .replyThread(type, count):
let textFont = Font.medium(17.0)
let textColor = titleTheme.rootController.navigationBar.primaryTextColor
if count > 0 {
var commentsPart: String
switch type {
case .comments:
commentsPart = self.strings.Conversation_TitleComments(Int32(count))
case .replies:
commentsPart = self.strings.Conversation_TitleReplies(Int32(count))
}
if let startIndex = commentsPart.firstIndex(of: "["), let endIndex = commentsPart.firstIndex(of: "]") {
commentsPart.removeSubrange(startIndex ... endIndex)
}
let rawTextAndRanges: (String, [(Int, NSRange)])
switch type {
case .comments:
rawTextAndRanges = self.strings.Conversation_TitleCommentsFormat("\(count)", commentsPart)
case .replies:
rawTextAndRanges = self.strings.Conversation_TitleRepliesFormat("\(count)", commentsPart)
}
let (rawText, ranges) = rawTextAndRanges
var textIndex = 0
var latestIndex = 0
for (index, range) in ranges {
var lowerSegmentIndex = range.lowerBound
if index != 0 {
lowerSegmentIndex = min(lowerSegmentIndex, latestIndex)
} else {
if latestIndex < range.lowerBound {
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: latestIndex) ..< rawText.index(rawText.startIndex, offsetBy: range.lowerBound)])
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: textColor)))
textIndex += 1
}
}
latestIndex = range.upperBound
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: lowerSegmentIndex) ..< rawText.index(rawText.startIndex, offsetBy: range.upperBound)])
if index == 0 {
segments.append(.number(count, NSAttributedString(string: part, font: textFont, textColor: textColor)))
} else {
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: textColor)))
textIndex += 1
}
}
if latestIndex < rawText.count {
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: latestIndex)...])
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: textColor)))
textIndex += 1
}
} else {
typeText = " "
switch type {
case .comments:
segments = [.text(0, NSAttributedString(string: strings.Conversation_TitleCommentsEmpty, font: textFont, textColor: textColor))]
case .replies:
segments = [.text(0, NSAttributedString(string: strings.Conversation_TitleRepliesEmpty, font: textFont, textColor: textColor))]
}
}
string = NSAttributedString(string: typeText, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
isEnabled = false
case .group:
string = NSAttributedString(string: "Feed", font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
case let .custom(text):
string = NSAttributedString(string: text, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
segments = [.text(0, NSAttributedString(string: text, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
}
if let string = string, self.titleNode.attributedText == nil || !self.titleNode.attributedText!.isEqual(to: string) {
self.titleNode.attributedText = string
self.setNeedsLayout()
var updated = false
if self.titleNode.segments != segments {
self.titleNode.segments = segments
updated = true
}
if titleLeftIcon != self.titleLeftIcon {
@ -169,13 +222,13 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
default:
self.titleLeftIconNode.image = nil
}
self.setNeedsLayout()
updated = true
}
if titleScamIcon != self.titleScamIcon {
self.titleScamIcon = titleScamIcon
self.titleCredibilityIconNode.image = titleScamIcon ? PresentationResourcesChatList.scamIcon(titleTheme, type: .regular) : nil
self.setNeedsLayout()
updated = true
}
if titleRightIcon != self.titleRightIcon {
@ -186,16 +239,22 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
default:
self.titleRightIconNode.image = nil
}
self.setNeedsLayout()
updated = true
}
self.isUserInteractionEnabled = isEnabled
self.button.isUserInteractionEnabled = isEnabled
self.updateStatus()
if !self.updateStatus() {
if updated {
if let (size, clearBounds) = self.validLayout {
self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
}
}
}
}
private func updateStatus() {
private func updateStatus() -> Bool {
var inputActivitiesAllowed = true
if let titleContent = self.titleContent {
switch titleContent {
@ -395,7 +454,12 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
break
}
self.accessibilityLabel = self.titleNode.attributedText?.string
var accessibilityText = ""
for segment in self.titleNode.segments {
accessibilityText.append(segment.attributedText.string)
}
self.accessibilityLabel = accessibilityText
self.accessibilityValue = state.string
} else {
self.accessibilityLabel = nil
@ -407,6 +471,9 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
if let (size, clearBounds) = self.validLayout {
self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.3, curve: .spring))
}
return true
} else {
return false
}
}
@ -419,10 +486,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
self.contentContainer = ASDisplayNode()
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isOpaque = false
self.titleNode = ImmediateAnimatedCountLabelNode()
self.titleLeftIconNode = ASImageNode()
self.titleLeftIconNode.isLayerBacked = true
@ -543,7 +607,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
let titleSideInset: CGFloat = 3.0
var titleFrame: CGRect
if size.height > 40.0 {
var titleSize = self.titleNode.updateLayout(CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height))
var titleSize = self.titleNode.updateLayout(size: CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height), animated: transition.isAnimated)
titleSize.width += credibilityIconWidth
let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .center)
let titleInfoSpacing: CGFloat = 0.0
@ -581,7 +645,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width + 3.0, y: 6.0), size: image.size)
}
} else {
let titleSize = self.titleNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0 - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0), height: size.height))
let titleSize = self.titleNode.updateLayout(size: CGSize(width: floor(clearBounds.width / 2.0 - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0), height: size.height), animated: transition.isAnimated)
let activitySize = self.activityNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0), height: size.height), alignment: .center)
let titleInfoSpacing: CGFloat = 8.0

View File

@ -144,7 +144,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {

View File

@ -133,7 +133,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {

View File

@ -1959,7 +1959,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
@ -5946,7 +5946,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
private var previousBackButtonBadge: ASDisplayNode?
private var currentBackButton: ASDisplayNode?
private var previousTitleNode: (ASDisplayNode, TextNode)?
private var previousTitleNode: (ASDisplayNode, ASDisplayNode)?
private var previousStatusNode: (ASDisplayNode, ASDisplayNode)?
private var didSetup: Bool = false

View File

@ -1198,7 +1198,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {

View File

@ -449,12 +449,12 @@ public final class WalletStrings: Equatable {
public var Wallet_Send_ConfirmationConfirm: String { return self._s[218]! }
public var Wallet_Created_ExportErrorTitle: String { return self._s[219]! }
public var Wallet_Info_TransactionPendingHeader: String { return self._s[220]! }
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
let form = getPluralizationForm(self.lc, value)
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue)
}
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
let form = getPluralizationForm(self.lc, value)
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue)

@ -1 +1 @@
Subproject commit af8d9313db00e216d5d1572369477e741fc4d461
Subproject commit c567dad74ffcea9df820809b2cdef90f544d68ca