mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Comments updates
This commit is contained in:
parent
1d8dd6202b
commit
6a0d0c1481
@ -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";
|
||||
|
22
submodules/AnimatedAvatarSetNode/BUCK
Normal file
22
submodules/AnimatedAvatarSetNode/BUCK
Normal 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",
|
||||
],
|
||||
)
|
22
submodules/AnimatedAvatarSetNode/BUILD
Normal file
22
submodules/AnimatedAvatarSetNode/BUILD
Normal 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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
16
submodules/AnimatedCountLabelNode/BUCK
Normal file
16
submodules/AnimatedCountLabelNode/BUCK
Normal 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",
|
||||
],
|
||||
)
|
16
submodules/AnimatedCountLabelNode/BUILD
Normal file
16
submodules/AnimatedCountLabelNode/BUILD
Normal 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",
|
||||
],
|
||||
)
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -206,6 +206,8 @@ swift_library(
|
||||
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
|
||||
"//submodules/GalleryData:GalleryData",
|
||||
"//submodules/ChatInterfaceState:ChatInterfaceState",
|
||||
"//submodules/AnimatedCountLabelNode:AnimatedCountLabelNode",
|
||||
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
Binary file not shown.
@ -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
|
||||
}
|
||||
|
@ -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: {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -451,7 +451,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, greetingStickerNode: {
|
||||
return nil
|
||||
}, openPeerContextMenu: { _, _, _, _ in
|
||||
}, openMessageReplies: { _, _ in
|
||||
}, openMessageReplies: { _, _, _ in
|
||||
}, openReplyThreadOriginalMessage: { _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
|
@ -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
|
||||
|
@ -144,7 +144,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
||||
}, greetingStickerNode: {
|
||||
return nil
|
||||
}, openPeerContextMenu: { _, _, _, _ in
|
||||
}, openMessageReplies: { _, _ in
|
||||
}, openMessageReplies: { _, _, _ in
|
||||
}, openReplyThreadOriginalMessage: { _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
|
@ -133,7 +133,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}, greetingStickerNode: {
|
||||
return nil
|
||||
}, openPeerContextMenu: { _, _, _, _ in
|
||||
}, openMessageReplies: { _, _ in
|
||||
}, openMessageReplies: { _, _, _ in
|
||||
}, openReplyThreadOriginalMessage: { _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
|
@ -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
|
||||
|
@ -1198,7 +1198,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, greetingStickerNode: {
|
||||
return nil
|
||||
}, openPeerContextMenu: { _, _, _, _ in
|
||||
}, openMessageReplies: { _, _ in
|
||||
}, openMessageReplies: { _, _, _ in
|
||||
}, openReplyThreadOriginalMessage: { _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
|
Binary file not shown.
@ -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
|
Loading…
x
Reference in New Issue
Block a user