mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +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.ContextViewThread" = "View Thread";
|
||||||
|
|
||||||
"Conversation.ViewReply" = "View Reply";
|
"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.MessageLeaveComment" = "Leave a Comment";
|
||||||
"Conversation.MessageLeaveCommentShort" = "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.InputTextPlaceholderReply" = "Reply";
|
||||||
"Conversation.InputTextPlaceholderComment" = "Comment";
|
"Conversation.InputTextPlaceholderComment" = "Comment";
|
||||||
|
|
||||||
"Conversation.TitleComments_1" = "%@ Comment";
|
|
||||||
"Conversation.TitleComments_any" = "%@ Comments";
|
|
||||||
"Conversation.TitleNoComments" = "Comments";
|
"Conversation.TitleNoComments" = "Comments";
|
||||||
|
|
||||||
"Conversation.ContextMenuBlock" = "Block User";
|
"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] = [:]
|
var topUpperHistoryBlockMessages: [PeerIdAndMessageNamespace: MessageId.Id] = [:]
|
||||||
|
|
||||||
final class MessageThreadStatsRecord {
|
final class MessageThreadStatsRecord {
|
||||||
var count: Int = 0
|
var removedCount: Int = 0
|
||||||
var peers: [ReplyThreadUserMessage] = []
|
var peers: [ReplyThreadUserMessage] = []
|
||||||
}
|
}
|
||||||
var messageThreadStatsDifferences: [MessageId: MessageThreadStatsRecord] = [:]
|
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] {
|
if let value = messageThreadStatsDifferences[threadMessageId] {
|
||||||
value.count += add - remove
|
value.removedCount += remove
|
||||||
if let addedMessagePeer = addedMessagePeer, let addedMessageId = addedMessageId {
|
if let addedMessagePeer = addedMessagePeer, let addedMessageId = addedMessageId {
|
||||||
value.peers.append(ReplyThreadUserMessage(id: addedMessagePeer, messageId: addedMessageId, isOutgoing: isOutgoing))
|
value.peers.append(ReplyThreadUserMessage(id: addedMessagePeer, messageId: addedMessageId, isOutgoing: isOutgoing))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let value = MessageThreadStatsRecord()
|
let value = MessageThreadStatsRecord()
|
||||||
messageThreadStatsDifferences[threadMessageId] = value
|
messageThreadStatsDifferences[threadMessageId] = value
|
||||||
value.count = add - remove
|
value.removedCount = remove
|
||||||
if let addedMessagePeer = addedMessagePeer, let addedMessageId = addedMessageId {
|
if let addedMessagePeer = addedMessagePeer, let addedMessageId = addedMessageId {
|
||||||
value.peers.append(ReplyThreadUserMessage(id: addedMessagePeer, messageId: addedMessageId, isOutgoing: isOutgoing))
|
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)
|
let messageThreadId = makeThreadIdMessageId(peerId: message.id.peerId, threadId: threadId)
|
||||||
if id.peerId.namespace == Namespaces.Peer.CloudChannel {
|
if id.peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||||
if !transaction.messageExists(id: id) {
|
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):
|
case let .DeleteMessages(ids):
|
||||||
deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: ids, manualAddMessageThreadStatsDifference: { id, add, remove in
|
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):
|
case let .UpdateMinAvailableMessage(id):
|
||||||
if let message = transaction.getMessage(id) {
|
if let message = transaction.getMessage(id) {
|
||||||
@ -3003,7 +3003,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
for (threadMessageId, difference) in messageThreadStatsDifferences {
|
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 {
|
if !peerActivityTimestamps.isEmpty {
|
||||||
|
@ -234,7 +234,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
|||||||
if let threadId = updatedMessage.threadId {
|
if let threadId = updatedMessage.threadId {
|
||||||
let messageThreadId = makeThreadIdMessageId(peerId: updatedMessage.id.peerId, threadId: threadId)
|
let messageThreadId = makeThreadIdMessageId(peerId: updatedMessage.id.peerId, threadId: threadId)
|
||||||
if let authorId = updatedMessage.authorId {
|
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 {
|
if let manualAddMessageThreadStatsDifference = manualAddMessageThreadStatsDifference {
|
||||||
manualAddMessageThreadStatsDifference(messageThreadId, 0, 1)
|
manualAddMessageThreadStatsDifference(messageThreadId, 0, 1)
|
||||||
} else {
|
} 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 struct ChatReplyThreadMessage: Equatable {
|
||||||
|
public enum Anchor {
|
||||||
|
case automatic
|
||||||
|
case lowerBound
|
||||||
|
}
|
||||||
|
|
||||||
public var messageId: MessageId
|
public var messageId: MessageId
|
||||||
public var isChannelPost: Bool
|
public var isChannelPost: Bool
|
||||||
public var maxMessage: MessageId?
|
public var maxMessage: MessageId?
|
||||||
public var maxReadIncomingMessageId: MessageId?
|
public var maxReadIncomingMessageId: MessageId?
|
||||||
public var maxReadOutgoingMessageId: MessageId?
|
public var maxReadOutgoingMessageId: MessageId?
|
||||||
public var initialFilledHoles: IndexSet
|
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.messageId = messageId
|
||||||
self.isChannelPost = isChannelPost
|
self.isChannelPost = isChannelPost
|
||||||
self.maxMessage = maxMessage
|
self.maxMessage = maxMessage
|
||||||
self.maxReadIncomingMessageId = maxReadIncomingMessageId
|
self.maxReadIncomingMessageId = maxReadIncomingMessageId
|
||||||
self.maxReadOutgoingMessageId = maxReadOutgoingMessageId
|
self.maxReadOutgoingMessageId = maxReadOutgoingMessageId
|
||||||
self.initialFilledHoles = initialFilledHoles
|
self.initialFilledHoles = initialFilledHoles
|
||||||
|
self.initialAnchor = initialAnchor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -521,12 +528,18 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
|
|||||||
let discussionMessage = Promise<DiscussionMessage?>()
|
let discussionMessage = Promise<DiscussionMessage?>()
|
||||||
discussionMessage.set(discussionMessageSignal)
|
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)
|
|> take(1)
|
||||||
|> castError(FetchChannelReplyThreadMessageError.self)
|
|> 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 {
|
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 threadInput: FetchMessageHistoryHoleThreadInput = .threadFromChannel(channelMessageId: messageId)
|
||||||
var threadMessageId: MessageId?
|
var threadMessageId: MessageId?
|
||||||
transaction.scanMessageAttributes(peerId: replyInfo.commentsPeerId, namespace: Namespaces.Message.Cloud, limit: 1000, { id, attributes in
|
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 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)
|
|> castError(FetchChannelReplyThreadMessageError.self)
|
||||||
} else {
|
} else {
|
||||||
return discussionMessage.get()
|
return discussionMessage.get()
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> castError(FetchChannelReplyThreadMessageError.self)
|
|> 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 {
|
guard let discussionMessage = discussionMessage else {
|
||||||
return .fail(.generic)
|
return .fail(.generic)
|
||||||
}
|
}
|
||||||
|
|
||||||
let topMessageId = discussionMessage.messageId
|
let topMessageId = discussionMessage.messageId
|
||||||
let commentsPeerId = topMessageId.peerId
|
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
|
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 {
|
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 {
|
if let threadMessageId = threadMessageId {
|
||||||
var holes = transaction.getThreadIndexHoles(peerId: threadMessageId.peerId, threadId: makeMessageThreadId(threadMessageId), namespace: Namespaces.Message.Cloud)
|
var holes = transaction.getThreadIndexHoles(peerId: threadMessageId.peerId, threadId: makeMessageThreadId(threadMessageId), namespace: Namespaces.Message.Cloud)
|
||||||
holes.remove(integersIn: Int(maxMessageId.id + 1) ..< Int(Int32.max))
|
holes.remove(integersIn: Int(maxMessageId.id + 1) ..< Int(Int32.max))
|
||||||
@ -576,11 +605,18 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
|
|||||||
holes.formIntersection(historyHoles)
|
holes.formIntersection(historyHoles)
|
||||||
}
|
}
|
||||||
|
|
||||||
let anchor: HistoryViewInputAnchor
|
let inputAnchor: HistoryViewInputAnchor
|
||||||
if let aroundMessageId = aroundMessageId {
|
let initialAnchor: ChatReplyThreadMessage.Anchor
|
||||||
anchor = .message(aroundMessageId)
|
switch anchor {
|
||||||
} else {
|
case .lowerBound:
|
||||||
anchor = .upperBound
|
inputAnchor = .lowerBound
|
||||||
|
initialAnchor = .lowerBound
|
||||||
|
case .upperBound:
|
||||||
|
inputAnchor = .upperBound
|
||||||
|
initialAnchor = .automatic
|
||||||
|
case let .message(id):
|
||||||
|
inputAnchor = .message(id)
|
||||||
|
initialAnchor = .automatic
|
||||||
}
|
}
|
||||||
|
|
||||||
let testView = transaction.getMessagesHistoryViewState(
|
let testView = transaction.getMessagesHistoryViewState(
|
||||||
@ -593,24 +629,34 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
|
|||||||
Namespaces.Message.Cloud: holes
|
Namespaces.Message.Cloud: holes
|
||||||
]
|
]
|
||||||
)),
|
)),
|
||||||
count: 30,
|
count: 40,
|
||||||
clipHoles: true,
|
clipHoles: true,
|
||||||
anchor: anchor,
|
anchor: inputAnchor,
|
||||||
namespaces: .not(Namespaces.Message.allScheduled)
|
namespaces: .not(Namespaces.Message.allScheduled)
|
||||||
)
|
)
|
||||||
if !testView.isLoading {
|
if !testView.isLoading {
|
||||||
return .single(FetchMessageHistoryHoleResult(removedIndices: IndexSet(), strictRemovedIndices: IndexSet()))
|
return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(), strictRemovedIndices: IndexSet()), initialAnchor))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let direction: MessageHistoryViewRelativeHoleDirection
|
let direction: MessageHistoryViewRelativeHoleDirection
|
||||||
if let aroundMessageId = aroundMessageId {
|
let initialAnchor: ChatReplyThreadMessage.Anchor
|
||||||
direction = .aroundId(aroundMessageId)
|
switch anchor {
|
||||||
} else {
|
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))
|
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)
|
|> castError(FetchChannelReplyThreadMessageError.self)
|
||||||
|
|> map { result -> (FetchMessageHistoryHoleResult, ChatReplyThreadMessage.Anchor) in
|
||||||
|
return (result, initialAnchor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|> castError(FetchChannelReplyThreadMessageError.self)
|
|> castError(FetchChannelReplyThreadMessageError.self)
|
||||||
|> switchToLatest
|
|> switchToLatest
|
||||||
@ -622,10 +668,11 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
|
|||||||
|> castError(FetchChannelReplyThreadMessageError.self),
|
|> castError(FetchChannelReplyThreadMessageError.self),
|
||||||
preloadedHistory
|
preloadedHistory
|
||||||
)
|
)
|
||||||
|> mapToSignal { discussionMessage, initialFilledHoles -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
|
|> mapToSignal { discussionMessage, initialFilledHolesAndInitialAnchor -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
|
||||||
guard let discussionMessage = discussionMessage else {
|
guard let discussionMessage = discussionMessage else {
|
||||||
return .fail(.generic)
|
return .fail(.generic)
|
||||||
}
|
}
|
||||||
|
let (initialFilledHoles, initialAnchor) = initialFilledHolesAndInitialAnchor
|
||||||
return account.postbox.transaction { transaction -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
|
return account.postbox.transaction { transaction -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
|
||||||
for range in initialFilledHoles.strictRemovedIndices.rangeView {
|
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))
|
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,
|
maxMessage: discussionMessage.maxMessage,
|
||||||
maxReadIncomingMessageId: discussionMessage.maxReadIncomingMessageId,
|
maxReadIncomingMessageId: discussionMessage.maxReadIncomingMessageId,
|
||||||
maxReadOutgoingMessageId: discussionMessage.maxReadOutgoingMessageId,
|
maxReadOutgoingMessageId: discussionMessage.maxReadOutgoingMessageId,
|
||||||
initialFilledHoles: initialFilledHoles.removedIndices
|
initialFilledHoles: initialFilledHoles.removedIndices,
|
||||||
|
initialAnchor: initialAnchor
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|> castError(FetchChannelReplyThreadMessageError.self)
|
|> castError(FetchChannelReplyThreadMessageError.self)
|
||||||
|
@ -35,11 +35,11 @@ struct ReplyThreadUserMessage {
|
|||||||
var isOutgoing: Bool
|
var isOutgoing: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateMessageThreadStats(transaction: Transaction, threadMessageId: MessageId, difference: Int, addedMessagePeers: [ReplyThreadUserMessage]) {
|
func updateMessageThreadStats(transaction: Transaction, threadMessageId: MessageId, removedCount: Int, addedMessagePeers: [ReplyThreadUserMessage]) {
|
||||||
updateMessageThreadStatsInternal(transaction: transaction, threadMessageId: threadMessageId, difference: difference, addedMessagePeers: addedMessagePeers, allowChannel: false)
|
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 {
|
guard let channel = transaction.getPeer(threadMessageId.peerId) as? TelegramChannel else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -77,12 +77,21 @@ private func updateMessageThreadStatsInternal(transaction: Transaction, threadMe
|
|||||||
}
|
}
|
||||||
|
|
||||||
transaction.updateMessage(threadMessageId, update: { currentMessage in
|
transaction.updateMessage(threadMessageId, update: { currentMessage in
|
||||||
let countDifference = Int32(difference)
|
|
||||||
|
|
||||||
var attributes = currentMessage.attributes
|
var attributes = currentMessage.attributes
|
||||||
loop: for j in 0 ..< attributes.count {
|
loop: for j in 0 ..< attributes.count {
|
||||||
if let attribute = attributes[j] as? ReplyThreadMessageAttribute {
|
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 maxMessageId = attribute.maxMessageId
|
||||||
var maxReadMessageId = attribute.maxReadMessageId
|
var maxReadMessageId = attribute.maxReadMessageId
|
||||||
if let maxAddedId = addedMessagePeers.map({ $0.messageId.id }).max() {
|
if let maxAddedId = addedMessagePeers.map({ $0.messageId.id }).max() {
|
||||||
@ -109,6 +118,6 @@ private func updateMessageThreadStatsInternal(transaction: Transaction, threadMe
|
|||||||
})
|
})
|
||||||
|
|
||||||
if let channelThreadMessageId = channelThreadMessageId {
|
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/MessageReactionListUI:MessageReactionListUI",
|
||||||
"//submodules/SegmentedControlNode:SegmentedControlNode",
|
"//submodules/SegmentedControlNode:SegmentedControlNode",
|
||||||
"//submodules/AppBundle:AppBundle",
|
"//submodules/AppBundle:AppBundle",
|
||||||
#"//submodules/WalletUI:WalletUI",
|
|
||||||
#"//submodules/WalletCore:WalletCore",
|
|
||||||
"//submodules/Markdown:Markdown",
|
"//submodules/Markdown:Markdown",
|
||||||
"//submodules/SearchPeerMembers:SearchPeerMembers",
|
"//submodules/SearchPeerMembers:SearchPeerMembers",
|
||||||
"//submodules/WidgetItems:WidgetItems",
|
"//submodules/WidgetItems:WidgetItems",
|
||||||
@ -211,6 +209,8 @@ framework(
|
|||||||
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
|
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
|
||||||
"//submodules/GalleryData:GalleryData",
|
"//submodules/GalleryData:GalleryData",
|
||||||
"//submodules/ChatInterfaceState:ChatInterfaceState",
|
"//submodules/ChatInterfaceState:ChatInterfaceState",
|
||||||
|
"//submodules/AnimatedCountLabelNode:AnimatedCountLabelNode",
|
||||||
|
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
|
||||||
],
|
],
|
||||||
frameworks = [
|
frameworks = [
|
||||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||||
|
@ -206,6 +206,8 @@ swift_library(
|
|||||||
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
|
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
|
||||||
"//submodules/GalleryData:GalleryData",
|
"//submodules/GalleryData:GalleryData",
|
||||||
"//submodules/ChatInterfaceState:ChatInterfaceState",
|
"//submodules/ChatInterfaceState:ChatInterfaceState",
|
||||||
|
"//submodules/AnimatedCountLabelNode:AnimatedCountLabelNode",
|
||||||
|
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
Binary file not shown.
@ -178,6 +178,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
private var didSetChatLocationInfoReady = false
|
private var didSetChatLocationInfoReady = false
|
||||||
private let chatLocationInfoData: ChatLocationInfoData
|
private let chatLocationInfoData: ChatLocationInfoData
|
||||||
|
|
||||||
|
private let cachedDataReady = Promise<Bool>()
|
||||||
|
private var didSetCachedDataReady = false
|
||||||
|
|
||||||
private var presentationInterfaceState: ChatPresentationInterfaceState
|
private var presentationInterfaceState: ChatPresentationInterfaceState
|
||||||
|
|
||||||
private var chatTitleView: ChatTitleView?
|
private var chatTitleView: ChatTitleView?
|
||||||
@ -289,6 +292,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
|
|
||||||
private var checkedPeerChatServiceActions = false
|
private var checkedPeerChatServiceActions = false
|
||||||
|
|
||||||
|
private var willAppear = false
|
||||||
private var didAppear = false
|
private var didAppear = false
|
||||||
private var scheduledActivateInput = 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)
|
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)
|
strongSelf.presentInGlobalOverlay(contextController)
|
||||||
})
|
})
|
||||||
}, openMessageReplies: { [weak self] messageId, isChannelPost in
|
}, openMessageReplies: { [weak self] messageId, isChannelPost, displayModalProgress in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.openMessageReplies(messageId: messageId, isChannelPost: isChannelPost, atMessage: nil)
|
strongSelf.openMessageReplies(messageId: messageId, isChannelPost: isChannelPost, atMessage: nil, displayModalProgress: displayModalProgress)
|
||||||
}, openReplyThreadOriginalMessage: { [weak self] message in
|
}, openReplyThreadOriginalMessage: { [weak self] message in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
@ -2206,8 +2210,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
for attribute in message.attributes {
|
for attribute in message.attributes {
|
||||||
if let attribute = attribute as? SourceReferenceMessageAttribute {
|
if let attribute = attribute as? SourceReferenceMessageAttribute {
|
||||||
if let threadMessageId = threadMessageId {
|
if let threadMessageId = threadMessageId {
|
||||||
if let navigationController = strongSelf.navigationController as? NavigationController {
|
if let _ = strongSelf.navigationController as? NavigationController {
|
||||||
strongSelf.openMessageReplies(messageId: threadMessageId, isChannelPost: true, atMessage: attribute.messageId)
|
strongSelf.openMessageReplies(messageId: threadMessageId, isChannelPost: true, atMessage: attribute.messageId, displayModalProgress: true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId))
|
strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId))
|
||||||
@ -2673,14 +2677,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let text: String
|
strongSelf.chatTitleView?.titleContent = .replyThread(type: replyThreadType, count: count)
|
||||||
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)
|
|
||||||
|
|
||||||
let firstTime = strongSelf.peerView == nil
|
let firstTime = strongSelf.peerView == nil
|
||||||
strongSelf.peerView = peerView
|
strongSelf.peerView = peerView
|
||||||
@ -3264,8 +3261,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
} else if let _ = combinedInitialData.cachedData as? CachedSecretChatData {
|
} else if let _ = combinedInitialData.cachedData as? CachedSecretChatData {
|
||||||
}
|
}
|
||||||
|
|
||||||
if case .replyThread = strongSelf.chatLocation {
|
if case let .replyThread(replyThreadMessageId) = strongSelf.chatLocation {
|
||||||
pinnedMessageId = nil
|
pinnedMessageId = replyThreadMessageId.messageId
|
||||||
}
|
}
|
||||||
|
|
||||||
var pinnedMessage: Message?
|
var pinnedMessage: Message?
|
||||||
@ -3435,7 +3432,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
let callsDataUpdated = strongSelf.presentationInterfaceState.callsAvailable != callsAvailable || strongSelf.presentationInterfaceState.callsPrivate != callsPrivate
|
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 {
|
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
|
return state
|
||||||
.updatedPinnedMessageId(pinnedMessageId)
|
.updatedPinnedMessageId(pinnedMessageId)
|
||||||
.updatedPinnedMessage(pinnedMessage)
|
.updatedPinnedMessage(pinnedMessage)
|
||||||
@ -3478,6 +3475,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
.updatedSlowmodeState(slowmodeState)
|
.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
|
self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyState.get(), self._chatLocationInfoReady.get(), self.cachedDataReady.get(), initialData) |> map { _, chatLocationInfoReady, cachedDataReady, _ in
|
||||||
return chatLocationInfoReady
|
return chatLocationInfoReady && cachedDataReady
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.context.sharedContext.immediateExperimentalUISettings.crashOnLongQueries {
|
if self.context.sharedContext.immediateExperimentalUISettings.crashOnLongQueries {
|
||||||
@ -5396,6 +5398,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
override public func viewWillAppear(_ animated: Bool) {
|
override public func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
self.willAppear = true
|
||||||
|
|
||||||
if self.scheduledActivateInput {
|
if self.scheduledActivateInput {
|
||||||
self.scheduledActivateInput = false
|
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 {
|
guard let navigationController = self.navigationController as? NavigationController else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.controllerInteraction?.currentMessageWithLoadingReplyThread == messageId {
|
if !displayModalProgress, self.controllerInteraction?.currentMessageWithLoadingReplyThread == messageId {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8332,7 +8336,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
return EmptyDisposable
|
return EmptyDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
if controllerInteraction.currentMessageWithLoadingReplyThread != messageId {
|
if !displayModalProgress, controllerInteraction.currentMessageWithLoadingReplyThread != messageId {
|
||||||
let previousId = controllerInteraction.currentMessageWithLoadingReplyThread
|
let previousId = controllerInteraction.currentMessageWithLoadingReplyThread
|
||||||
controllerInteraction.currentMessageWithLoadingReplyThread = messageId
|
controllerInteraction.currentMessageWithLoadingReplyThread = messageId
|
||||||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(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 {
|
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if controllerInteraction.currentMessageWithLoadingReplyThread == messageId {
|
if !displayModalProgress, controllerInteraction.currentMessageWithLoadingReplyThread == messageId {
|
||||||
controllerInteraction.currentMessageWithLoadingReplyThread = nil
|
controllerInteraction.currentMessageWithLoadingReplyThread = nil
|
||||||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(messageId)
|
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.navigationActionDisposable.set((ChatControllerImpl.openMessageReplies(context: self.context, navigationController: navigationController, present: { [weak self] c, a in
|
||||||
self?.present(c, in: .window(.root), with: a)
|
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 {
|
|> afterDisposed {
|
||||||
progress.dispose()
|
progress.dispose()
|
||||||
}).start())
|
}).start())
|
||||||
@ -8390,6 +8394,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
let subject: ChatControllerSubject?
|
let subject: ChatControllerSubject?
|
||||||
if let atMessageId = atMessageId {
|
if let atMessageId = atMessageId {
|
||||||
subject = .message(atMessageId)
|
subject = .message(atMessageId)
|
||||||
|
} else if result.scrollToLowerBound {
|
||||||
|
subject = .message(MessageId(peerId: result.message.messageId.peerId, namespace: Namespaces.Message.Cloud, id: 1))
|
||||||
} else {
|
} else {
|
||||||
subject = nil
|
subject = nil
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ public final class ChatControllerInteraction {
|
|||||||
let animateDiceSuccess: () -> Void
|
let animateDiceSuccess: () -> Void
|
||||||
let greetingStickerNode: () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, () -> Void)?
|
let greetingStickerNode: () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, () -> Void)?
|
||||||
let openPeerContextMenu: (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
let openPeerContextMenu: (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
||||||
let openMessageReplies: (MessageId, Bool) -> Void
|
let openMessageReplies: (MessageId, Bool, Bool) -> Void
|
||||||
let openReplyThreadOriginalMessage: (Message) -> Void
|
let openReplyThreadOriginalMessage: (Message) -> Void
|
||||||
|
|
||||||
let requestMessageUpdate: (MessageId) -> Void
|
let requestMessageUpdate: (MessageId) -> Void
|
||||||
@ -197,7 +197,7 @@ public final class ChatControllerInteraction {
|
|||||||
animateDiceSuccess: @escaping () -> Void,
|
animateDiceSuccess: @escaping () -> Void,
|
||||||
greetingStickerNode: @escaping () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, () -> Void)?,
|
greetingStickerNode: @escaping () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, () -> Void)?,
|
||||||
openPeerContextMenu: @escaping (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void,
|
openPeerContextMenu: @escaping (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void,
|
||||||
openMessageReplies: @escaping (MessageId, Bool) -> Void,
|
openMessageReplies: @escaping (MessageId, Bool, Bool) -> Void,
|
||||||
openReplyThreadOriginalMessage: @escaping (Message) -> Void,
|
openReplyThreadOriginalMessage: @escaping (Message) -> Void,
|
||||||
requestMessageUpdate: @escaping (MessageId) -> Void,
|
requestMessageUpdate: @escaping (MessageId) -> Void,
|
||||||
cancelInteractiveKeyboardGestures: @escaping () -> Void,
|
cancelInteractiveKeyboardGestures: @escaping () -> Void,
|
||||||
@ -319,7 +319,7 @@ public final class ChatControllerInteraction {
|
|||||||
}, greetingStickerNode: {
|
}, greetingStickerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, openPeerContextMenu: { _, _, _, _ in
|
}, openPeerContextMenu: { _, _, _, _ in
|
||||||
}, openMessageReplies: { _, _ in
|
}, openMessageReplies: { _, _, _ in
|
||||||
}, openReplyThreadOriginalMessage: { _ in
|
}, openReplyThreadOriginalMessage: { _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
|
@ -1100,6 +1100,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
|
|
||||||
private func processDisplayedItemRangeChanged(displayedRange: ListViewDisplayedItemRange, transactionState: ChatHistoryTransactionOpaqueState) {
|
private func processDisplayedItemRangeChanged(displayedRange: ListViewDisplayedItemRange, transactionState: ChatHistoryTransactionOpaqueState) {
|
||||||
let historyView = transactionState.historyView
|
let historyView = transactionState.historyView
|
||||||
|
var isTopReplyThreadMessageShownValue = false
|
||||||
if let visible = displayedRange.visibleRange {
|
if let visible = displayedRange.visibleRange {
|
||||||
let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)
|
let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)
|
||||||
if indexRange.0 > indexRange.1 {
|
if indexRange.0 > indexRange.1 {
|
||||||
@ -1120,8 +1121,6 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
var messagesWithPreloadableMediaToEarlier: [(Message, Media)] = []
|
var messagesWithPreloadableMediaToEarlier: [(Message, Media)] = []
|
||||||
var messagesWithPreloadableMediaToLater: [(Message, Media)] = []
|
var messagesWithPreloadableMediaToLater: [(Message, Media)] = []
|
||||||
|
|
||||||
var isTopReplyThreadMessageShownValue = false
|
|
||||||
|
|
||||||
if indexRange.0 <= indexRange.1 {
|
if indexRange.0 <= indexRange.1 {
|
||||||
for i in (indexRange.0 ... indexRange.1) {
|
for i in (indexRange.0 ... indexRange.1) {
|
||||||
switch historyView.filteredEntries[i] {
|
switch historyView.filteredEntries[i] {
|
||||||
@ -1320,9 +1319,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
self.maxVisibleMessageIndexUpdated?(maxOverallIndex)
|
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 let loaded = displayedRange.loadedRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last {
|
||||||
if loaded.firstIndex < 5 && historyView.originalView.laterId != nil {
|
if loaded.firstIndex < 5 && historyView.originalView.laterId != nil {
|
||||||
|
@ -85,41 +85,52 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, context: A
|
|||||||
}
|
}
|
||||||
var scrollPosition: ChatHistoryViewScrollPosition?
|
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
|
let aroundIndex = maxReadIndex
|
||||||
scrollPosition = .unread(index: maxReadIndex)
|
scrollPosition = .unread(index: maxReadIndex)
|
||||||
|
|
||||||
var targetIndex = 0
|
if case .peer = chatLocation {
|
||||||
for i in 0 ..< view.entries.count {
|
var targetIndex = 0
|
||||||
if view.entries[i].index >= aroundIndex {
|
for i in 0 ..< view.entries.count {
|
||||||
targetIndex = i
|
if view.entries[i].index >= aroundIndex {
|
||||||
break
|
targetIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
let maxIndex = targetIndex + count / 2
|
||||||
let maxIndex = targetIndex + count / 2
|
let minIndex = targetIndex - count / 2
|
||||||
let minIndex = targetIndex - count / 2
|
if minIndex <= 0 && view.holeEarlier {
|
||||||
if minIndex <= 0 && view.holeEarlier {
|
|
||||||
fadeIn = true
|
|
||||||
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
|
|
||||||
}
|
|
||||||
if maxIndex >= targetIndex {
|
|
||||||
if view.holeLater {
|
|
||||||
fadeIn = true
|
fadeIn = true
|
||||||
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
|
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
|
||||||
}
|
}
|
||||||
if view.holeEarlier {
|
if maxIndex >= targetIndex {
|
||||||
var incomingCount: Int32 = 0
|
if view.holeLater {
|
||||||
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
|
fadeIn = true
|
||||||
return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType))
|
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 {
|
} else if view.isAddedToChatList, let historyScrollState = (initialData?.chatInterfaceState as? ChatInterfaceState)?.historyScrollState, tagMask == nil {
|
||||||
@ -294,6 +305,7 @@ struct ReplyThreadInfo {
|
|||||||
var message: ChatReplyThreadMessage
|
var message: ChatReplyThreadMessage
|
||||||
var isChannelPost: Bool
|
var isChannelPost: Bool
|
||||||
var isEmpty: Bool
|
var isEmpty: Bool
|
||||||
|
var scrollToLowerBound: Bool
|
||||||
var contextHolder: Atomic<ChatLocationContextHolder?>
|
var contextHolder: Atomic<ChatLocationContextHolder?>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,9 +317,7 @@ enum ReplyThreadSubject {
|
|||||||
func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThreadSubject, atMessageId: MessageId?) -> Signal<ReplyThreadInfo, FetchChannelReplyThreadMessageError> {
|
func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThreadSubject, atMessageId: MessageId?) -> Signal<ReplyThreadInfo, FetchChannelReplyThreadMessageError> {
|
||||||
let message: Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError>
|
let message: Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError>
|
||||||
switch subject {
|
switch subject {
|
||||||
case let .channelPost(messageId):
|
case .channelPost(let messageId), .groupMessage(let messageId):
|
||||||
message = fetchChannelReplyThreadMessage(account: context.account, messageId: messageId, atMessageId: atMessageId)
|
|
||||||
case let .groupMessage(messageId):
|
|
||||||
message = fetchChannelReplyThreadMessage(account: context.account, messageId: messageId, atMessageId: atMessageId)
|
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 chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
|
||||||
|
|
||||||
let input: ChatHistoryLocationInput
|
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(
|
input = ChatHistoryLocationInput(
|
||||||
content: .InitialSearch(location: .id(atMessageId), count: 30),
|
content: .Navigation(index: .lowerBound, anchorIndex: .lowerBound, count: 40),
|
||||||
id: 0
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
input = ChatHistoryLocationInput(
|
|
||||||
content: .Initial(count: 30),
|
|
||||||
id: 0
|
id: 0
|
||||||
)
|
)
|
||||||
|
scrollToLowerBound = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let preloadSignal = preloadedChatHistoryViewForLocation(
|
let preloadSignal = preloadedChatHistoryViewForLocation(
|
||||||
@ -359,6 +380,7 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea
|
|||||||
message: replyThreadMessage,
|
message: replyThreadMessage,
|
||||||
isChannelPost: replyThreadMessage.isChannelPost,
|
isChannelPost: replyThreadMessage.isChannelPost,
|
||||||
isEmpty: isEmpty,
|
isEmpty: isEmpty,
|
||||||
|
scrollToLowerBound: scrollToLowerBound,
|
||||||
contextHolder: chatLocationContextHolder
|
contextHolder: chatLocationContextHolder
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -601,7 +601,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
|
|||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replies"), color: theme.actionSheet.primaryTextColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replies"), color: theme.actionSheet.primaryTextColor)
|
||||||
}, action: { c, _ in
|
}, action: { c, _ in
|
||||||
c.dismiss(completion: {
|
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 {
|
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let _ = attribute as? ReplyThreadMessageAttribute {
|
if let _ = attribute as? ReplyThreadMessageAttribute {
|
||||||
item.controllerInteraction.openMessageReplies(item.message.id, true)
|
item.controllerInteraction.openMessageReplies(item.message.id, true, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,15 +8,18 @@ import TelegramCore
|
|||||||
import SyncCore
|
import SyncCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import RadialStatusNode
|
import RadialStatusNode
|
||||||
|
import AnimatedCountLabelNode
|
||||||
|
import AnimatedAvatarSetNode
|
||||||
|
|
||||||
final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
||||||
private let separatorNode: ASDisplayNode
|
private let separatorNode: ASDisplayNode
|
||||||
private let textNode: TextNode
|
private let countNode: AnimatedCountLabelNode
|
||||||
private let alternativeTextNode: TextNode
|
private let alternativeCountNode: AnimatedCountLabelNode
|
||||||
private let iconNode: ASImageNode
|
private let iconNode: ASImageNode
|
||||||
private let arrowNode: ASImageNode
|
private let arrowNode: ASImageNode
|
||||||
private let buttonNode: HighlightTrackingButtonNode
|
private let buttonNode: HighlightTrackingButtonNode
|
||||||
private let avatarsNode: MergedAvatarsNode
|
private let avatarsContext: AnimatedAvatarSetContext
|
||||||
|
private let avatarsNode: AnimatedAvatarSetNode
|
||||||
private let unreadIconNode: ASImageNode
|
private let unreadIconNode: ASImageNode
|
||||||
private var statusNode: RadialStatusNode?
|
private var statusNode: RadialStatusNode?
|
||||||
|
|
||||||
@ -24,17 +27,8 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
self.separatorNode = ASDisplayNode()
|
self.separatorNode = ASDisplayNode()
|
||||||
self.separatorNode.isUserInteractionEnabled = false
|
self.separatorNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.textNode = TextNode()
|
self.countNode = AnimatedCountLabelNode()
|
||||||
self.textNode.isUserInteractionEnabled = false
|
self.alternativeCountNode = AnimatedCountLabelNode()
|
||||||
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.iconNode = ASImageNode()
|
self.iconNode = ASImageNode()
|
||||||
self.iconNode.displaysAsynchronously = false
|
self.iconNode.displaysAsynchronously = false
|
||||||
@ -51,7 +45,8 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
self.arrowNode.displayWithoutProcessing = true
|
self.arrowNode.displayWithoutProcessing = true
|
||||||
self.arrowNode.isUserInteractionEnabled = false
|
self.arrowNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.avatarsNode = MergedAvatarsNode()
|
self.avatarsContext = AnimatedAvatarSetContext()
|
||||||
|
self.avatarsNode = AnimatedAvatarSetNode()
|
||||||
self.avatarsNode.isUserInteractionEnabled = false
|
self.avatarsNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.buttonNode = HighlightTrackingButtonNode()
|
self.buttonNode = HighlightTrackingButtonNode()
|
||||||
@ -59,8 +54,8 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.buttonNode.addSubnode(self.separatorNode)
|
self.buttonNode.addSubnode(self.separatorNode)
|
||||||
self.buttonNode.addSubnode(self.textNode)
|
self.buttonNode.addSubnode(self.countNode)
|
||||||
self.buttonNode.addSubnode(self.alternativeTextNode)
|
self.buttonNode.addSubnode(self.alternativeCountNode)
|
||||||
self.buttonNode.addSubnode(self.iconNode)
|
self.buttonNode.addSubnode(self.iconNode)
|
||||||
self.buttonNode.addSubnode(self.unreadIconNode)
|
self.buttonNode.addSubnode(self.unreadIconNode)
|
||||||
self.buttonNode.addSubnode(self.arrowNode)
|
self.buttonNode.addSubnode(self.arrowNode)
|
||||||
@ -70,12 +65,7 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
let nodes: [ASDisplayNode] = [
|
let nodes: [ASDisplayNode] = [
|
||||||
strongSelf.textNode,
|
strongSelf.buttonNode
|
||||||
strongSelf.alternativeTextNode,
|
|
||||||
strongSelf.iconNode,
|
|
||||||
strongSelf.avatarsNode,
|
|
||||||
strongSelf.unreadIconNode,
|
|
||||||
strongSelf.arrowNode,
|
|
||||||
]
|
]
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
if highlighted {
|
if highlighted {
|
||||||
@ -103,13 +93,13 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
if item.message.id.peerId.isReplies {
|
if item.message.id.peerId.isReplies {
|
||||||
item.controllerInteraction.openReplyThreadOriginalMessage(item.message)
|
item.controllerInteraction.openReplyThreadOriginalMessage(item.message)
|
||||||
} else {
|
} 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))) {
|
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 makeCountLayout = self.countNode.asyncLayout()
|
||||||
let alternativeTextLayout = TextNode.asyncLayout(self.alternativeTextNode)
|
let makeAlternativeCountLayout = self.alternativeCountNode.asyncLayout()
|
||||||
|
|
||||||
return { item, layoutConstants, preparePosition, _, constrainedSize in
|
return { item, layoutConstants, preparePosition, _, constrainedSize in
|
||||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
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 messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
|
||||||
let rawAlternativeText: String
|
|
||||||
|
let textFont = item.presentationData.messageFont
|
||||||
|
|
||||||
|
let rawSegments: [AnimatedCountLabelNode.Segment]
|
||||||
|
let rawAlternativeSegments: [AnimatedCountLabelNode.Segment]
|
||||||
|
|
||||||
if item.message.id.peerId.isReplies {
|
if item.message.id.peerId.isReplies {
|
||||||
rawText = item.presentationData.strings.Conversation_ViewReply
|
rawSegments = [.text(100, NSAttributedString(string: item.presentationData.strings.Conversation_ViewReply, font: textFont, textColor: messageTheme.accentTextColor))]
|
||||||
rawAlternativeText = rawText
|
rawAlternativeSegments = rawSegments
|
||||||
} else if dateReplies > 0 {
|
} else if dateReplies > 0 {
|
||||||
rawText = item.presentationData.strings.Conversation_MessageViewComments(Int32(dateReplies))
|
var commentsPart = item.presentationData.strings.Conversation_MessageViewComments(Int32(dateReplies))
|
||||||
rawAlternativeText = rawText
|
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 {
|
} else {
|
||||||
rawText = item.presentationData.strings.Conversation_MessageLeaveComment
|
rawSegments = [.text(100, NSAttributedString(string: item.presentationData.strings.Conversation_MessageLeaveComment, font: textFont, textColor: messageTheme.accentTextColor))]
|
||||||
rawAlternativeText = item.presentationData.strings.Conversation_MessageLeaveCommentShort
|
rawAlternativeSegments = [.text(100, NSAttributedString(string: item.presentationData.strings.Conversation_MessageLeaveCommentShort, font: textFont, textColor: messageTheme.accentTextColor))]
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageSize: CGFloat = 30.0
|
let imageSize: CGFloat = 30.0
|
||||||
@ -169,23 +200,16 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
} else {
|
} else {
|
||||||
textLeftInset = 15.0 + imageSize * min(1.0, CGFloat(replyPeers.count)) + (imageSpacing) * max(0.0, min(2.0, CGFloat(replyPeers.count - 1)))
|
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 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)
|
var textFrame = CGRect(origin: CGPoint(x: -textInsets.left + textLeftInset - 2.0, y: -textInsets.top + 5.0 + topOffset), size: countLayout.size)
|
||||||
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 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))
|
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)
|
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 {
|
if let strongSelf = self {
|
||||||
strongSelf.item = item
|
strongSelf.item = item
|
||||||
|
|
||||||
strongSelf.textNode.displaysAsynchronously = !item.presentationData.isPreview
|
let transition: ContainedViewLayoutTransition
|
||||||
strongSelf.alternativeTextNode.displaysAsynchronously = !item.presentationData.isPreview
|
if animation.isAnimated {
|
||||||
|
transition = .animated(duration: 0.2, curve: .easeInOut)
|
||||||
|
} else {
|
||||||
|
transition = .immediate
|
||||||
|
}
|
||||||
|
|
||||||
strongSelf.textNode.isHidden = textLayout.truncated
|
strongSelf.countNode.isHidden = countLayout.isTruncated
|
||||||
strongSelf.alternativeTextNode.isHidden = !strongSelf.textNode.isHidden
|
strongSelf.alternativeCountNode.isHidden = !strongSelf.countNode.isHidden
|
||||||
|
|
||||||
let _ = textApply()
|
let _ = countApply(animation.isAnimated)
|
||||||
let _ = alternativeTextApply()
|
let _ = alternativeCountApply(animation.isAnimated)
|
||||||
|
|
||||||
let adjustedTextFrame = textFrame
|
let adjustedTextFrame = textFrame
|
||||||
|
|
||||||
strongSelf.textNode.frame = adjustedTextFrame
|
if strongSelf.countNode.frame.isEmpty {
|
||||||
strongSelf.alternativeTextNode.frame = CGRect(origin: adjustedTextFrame.origin, size: alternativeTextLayout.size)
|
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
|
let effectiveTextFrame: CGRect
|
||||||
if !strongSelf.alternativeTextNode.isHidden {
|
if !strongSelf.alternativeCountNode.isHidden {
|
||||||
effectiveTextFrame = strongSelf.alternativeTextNode.frame
|
effectiveTextFrame = strongSelf.alternativeCountNode.frame
|
||||||
} else {
|
} else {
|
||||||
effectiveTextFrame = strongSelf.textNode.frame
|
effectiveTextFrame = strongSelf.countNode.frame
|
||||||
}
|
}
|
||||||
|
|
||||||
if let iconImage = iconImage {
|
if let iconImage = iconImage {
|
||||||
@ -247,17 +284,31 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
if let arrowImage = arrowImage {
|
if let arrowImage = arrowImage {
|
||||||
strongSelf.arrowNode.image = arrowImage
|
strongSelf.arrowNode.image = arrowImage
|
||||||
let arrowFrame = CGRect(origin: CGPoint(x: boundingWidth - 33.0, y: 6.0 + topOffset), size: arrowImage.size)
|
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 {
|
if let unreadIconImage = unreadIconImage {
|
||||||
strongSelf.unreadIconNode.image = 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
|
if strongSelf.unreadIconNode.alpha.isZero != !hasUnseenReplies {
|
||||||
|
transition.updateAlpha(node: strongSelf.unreadIconNode, alpha: hasUnseenReplies ? 1.0 : 0.0)
|
||||||
strongSelf.iconNode.isHidden = !replyPeers.isEmpty
|
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
|
let hasActivity = item.controllerInteraction.currentMessageWithLoadingReplyThread == item.message.id
|
||||||
|
|
||||||
@ -272,7 +323,14 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
strongSelf.buttonNode.addSubnode(statusNode)
|
strongSelf.buttonNode.addSubnode(statusNode)
|
||||||
}
|
}
|
||||||
let statusSize = CGSize(width: 20.0, height: 20.0)
|
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: {})
|
statusNode.transitionToState(.progress(color: messageTheme.accentTextColor, lineWidth: 1.5, value: nil, cancelEnabled: false), animated: false, synchronous: false, completion: {})
|
||||||
} else {
|
} else {
|
||||||
strongSelf.arrowNode.isHidden = false
|
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.frame = avatarsFrame
|
||||||
strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size)
|
//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.update(context: item.context, peers: replyPeers, synchronousLoad: synchronousLoad, imageSize: imageSize, imageSpacing: imageSpacing, borderWidth: 2.0 - UIScreenPixel)
|
||||||
|
|
||||||
strongSelf.separatorNode.backgroundColor = messageTheme.polls.separator
|
strongSelf.separatorNode.backgroundColor = messageTheme.polls.separator
|
||||||
strongSelf.separatorNode.isHidden = !displaySeparator
|
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 {
|
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let _ = attribute as? ReplyThreadMessageAttribute {
|
if let _ = attribute as? ReplyThreadMessageAttribute {
|
||||||
item.controllerInteraction.openMessageReplies(item.message.id, true)
|
item.controllerInteraction.openMessageReplies(item.message.id, true, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -814,7 +814,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
|
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let _ = attribute as? ReplyThreadMessageAttribute {
|
if let _ = attribute as? ReplyThreadMessageAttribute {
|
||||||
item.controllerInteraction.openMessageReplies(item.message.id, true)
|
item.controllerInteraction.openMessageReplies(item.message.id, true, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -451,7 +451,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||||||
}, greetingStickerNode: {
|
}, greetingStickerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, openPeerContextMenu: { _, _, _, _ in
|
}, openPeerContextMenu: { _, _, _, _ in
|
||||||
}, openMessageReplies: { _, _ in
|
}, openMessageReplies: { _, _, _ in
|
||||||
}, openReplyThreadOriginalMessage: { _ in
|
}, openReplyThreadOriginalMessage: { _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
|
@ -16,16 +16,16 @@ import ChatTitleActivityNode
|
|||||||
import LocalizedPeerData
|
import LocalizedPeerData
|
||||||
import PhoneNumberFormat
|
import PhoneNumberFormat
|
||||||
import ChatTitleActivityNode
|
import ChatTitleActivityNode
|
||||||
|
import AnimatedCountLabelNode
|
||||||
|
|
||||||
enum ChatTitleContent {
|
enum ChatTitleContent {
|
||||||
enum ReplyThreadType {
|
enum ReplyThreadType {
|
||||||
case replies
|
|
||||||
case comments
|
case comments
|
||||||
|
case replies
|
||||||
}
|
}
|
||||||
|
|
||||||
case peer(peerView: PeerView, onlineMemberCount: Int32?, isScheduledMessages: Bool)
|
case peer(peerView: PeerView, onlineMemberCount: Int32?, isScheduledMessages: Bool)
|
||||||
case replyThread(type: ReplyThreadType, text: String)
|
case replyThread(type: ReplyThreadType, count: Int)
|
||||||
case group([Peer])
|
|
||||||
case custom(String)
|
case custom(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
private var nameDisplayOrder: PresentationPersonNameOrder
|
private var nameDisplayOrder: PresentationPersonNameOrder
|
||||||
|
|
||||||
private let contentContainer: ASDisplayNode
|
private let contentContainer: ASDisplayNode
|
||||||
let titleNode: ImmediateTextNode
|
let titleNode: ImmediateAnimatedCountLabelNode
|
||||||
let titleLeftIconNode: ASImageNode
|
let titleLeftIconNode: ASImageNode
|
||||||
let titleRightIconNode: ASImageNode
|
let titleRightIconNode: ASImageNode
|
||||||
let titleCredibilityIconNode: ASImageNode
|
let titleCredibilityIconNode: ASImageNode
|
||||||
@ -100,7 +100,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
if let titleContent = self.titleContent {
|
if let titleContent = self.titleContent {
|
||||||
let titleTheme = self.hasEmbeddedTitleContent ? defaultDarkPresentationTheme : self.theme
|
let titleTheme = self.hasEmbeddedTitleContent ? defaultDarkPresentationTheme : self.theme
|
||||||
|
|
||||||
var string: NSAttributedString?
|
var segments: [AnimatedCountLabelNode.Segment] = []
|
||||||
var titleLeftIcon: ChatTitleIcon = .none
|
var titleLeftIcon: ChatTitleIcon = .none
|
||||||
var titleRightIcon: ChatTitleIcon = .none
|
var titleRightIcon: ChatTitleIcon = .none
|
||||||
var titleScamIcon = false
|
var titleScamIcon = false
|
||||||
@ -109,24 +109,24 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
case let .peer(peerView, _, isScheduledMessages):
|
case let .peer(peerView, _, isScheduledMessages):
|
||||||
if peerView.peerId.isReplies {
|
if peerView.peerId.isReplies {
|
||||||
let typeText: String = self.strings.DialogList_Replies
|
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
|
isEnabled = false
|
||||||
} else if isScheduledMessages {
|
} else if isScheduledMessages {
|
||||||
if peerView.peerId == self.account.peerId {
|
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 {
|
} 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
|
isEnabled = false
|
||||||
} else {
|
} else {
|
||||||
if let peer = peerViewMainPeer(peerView) {
|
if let peer = peerViewMainPeer(peerView) {
|
||||||
if peerView.peerId == self.account.peerId {
|
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 {
|
} else {
|
||||||
if !peerView.peerIsContact, let user = peer as? TelegramUser, !user.flags.contains(.isSupport), user.botInfo == nil, let phone = user.phone, !phone.isEmpty {
|
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 {
|
} 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
|
titleScamIcon = peer.isScam
|
||||||
@ -140,25 +140,78 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .replyThread(type, text):
|
case let .replyThread(type, count):
|
||||||
let typeText: String
|
let textFont = Font.medium(17.0)
|
||||||
if !text.isEmpty {
|
let textColor = titleTheme.rootController.navigationBar.primaryTextColor
|
||||||
typeText = text
|
|
||||||
|
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 {
|
} 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
|
isEnabled = false
|
||||||
case .group:
|
|
||||||
string = NSAttributedString(string: "Feed", font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
|
|
||||||
case let .custom(text):
|
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) {
|
var updated = false
|
||||||
self.titleNode.attributedText = string
|
|
||||||
self.setNeedsLayout()
|
if self.titleNode.segments != segments {
|
||||||
|
self.titleNode.segments = segments
|
||||||
|
updated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if titleLeftIcon != self.titleLeftIcon {
|
if titleLeftIcon != self.titleLeftIcon {
|
||||||
@ -169,13 +222,13 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
default:
|
default:
|
||||||
self.titleLeftIconNode.image = nil
|
self.titleLeftIconNode.image = nil
|
||||||
}
|
}
|
||||||
self.setNeedsLayout()
|
updated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if titleScamIcon != self.titleScamIcon {
|
if titleScamIcon != self.titleScamIcon {
|
||||||
self.titleScamIcon = titleScamIcon
|
self.titleScamIcon = titleScamIcon
|
||||||
self.titleCredibilityIconNode.image = titleScamIcon ? PresentationResourcesChatList.scamIcon(titleTheme, type: .regular) : nil
|
self.titleCredibilityIconNode.image = titleScamIcon ? PresentationResourcesChatList.scamIcon(titleTheme, type: .regular) : nil
|
||||||
self.setNeedsLayout()
|
updated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if titleRightIcon != self.titleRightIcon {
|
if titleRightIcon != self.titleRightIcon {
|
||||||
@ -186,16 +239,22 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
default:
|
default:
|
||||||
self.titleRightIconNode.image = nil
|
self.titleRightIconNode.image = nil
|
||||||
}
|
}
|
||||||
self.setNeedsLayout()
|
updated = true
|
||||||
}
|
}
|
||||||
self.isUserInteractionEnabled = isEnabled
|
self.isUserInteractionEnabled = isEnabled
|
||||||
self.button.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
|
var inputActivitiesAllowed = true
|
||||||
if let titleContent = self.titleContent {
|
if let titleContent = self.titleContent {
|
||||||
switch titleContent {
|
switch titleContent {
|
||||||
@ -395,7 +454,12 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
break
|
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
|
self.accessibilityValue = state.string
|
||||||
} else {
|
} else {
|
||||||
self.accessibilityLabel = nil
|
self.accessibilityLabel = nil
|
||||||
@ -407,6 +471,9 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
if let (size, clearBounds) = self.validLayout {
|
if let (size, clearBounds) = self.validLayout {
|
||||||
self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.3, curve: .spring))
|
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.contentContainer = ASDisplayNode()
|
||||||
|
|
||||||
self.titleNode = ImmediateTextNode()
|
self.titleNode = ImmediateAnimatedCountLabelNode()
|
||||||
self.titleNode.displaysAsynchronously = false
|
|
||||||
self.titleNode.maximumNumberOfLines = 1
|
|
||||||
self.titleNode.isOpaque = false
|
|
||||||
|
|
||||||
self.titleLeftIconNode = ASImageNode()
|
self.titleLeftIconNode = ASImageNode()
|
||||||
self.titleLeftIconNode.isLayerBacked = true
|
self.titleLeftIconNode.isLayerBacked = true
|
||||||
@ -543,7 +607,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
let titleSideInset: CGFloat = 3.0
|
let titleSideInset: CGFloat = 3.0
|
||||||
var titleFrame: CGRect
|
var titleFrame: CGRect
|
||||||
if size.height > 40.0 {
|
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
|
titleSize.width += credibilityIconWidth
|
||||||
let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .center)
|
let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .center)
|
||||||
let titleInfoSpacing: CGFloat = 0.0
|
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)
|
self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width + 3.0, y: 6.0), size: image.size)
|
||||||
}
|
}
|
||||||
} else {
|
} 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 activitySize = self.activityNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0), height: size.height), alignment: .center)
|
||||||
|
|
||||||
let titleInfoSpacing: CGFloat = 8.0
|
let titleInfoSpacing: CGFloat = 8.0
|
||||||
|
@ -144,7 +144,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
|||||||
}, greetingStickerNode: {
|
}, greetingStickerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, openPeerContextMenu: { _, _, _, _ in
|
}, openPeerContextMenu: { _, _, _, _ in
|
||||||
}, openMessageReplies: { _, _ in
|
}, openMessageReplies: { _, _, _ in
|
||||||
}, openReplyThreadOriginalMessage: { _ in
|
}, openReplyThreadOriginalMessage: { _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
|
@ -133,7 +133,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
|||||||
}, greetingStickerNode: {
|
}, greetingStickerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, openPeerContextMenu: { _, _, _, _ in
|
}, openPeerContextMenu: { _, _, _, _ in
|
||||||
}, openMessageReplies: { _, _ in
|
}, openMessageReplies: { _, _, _ in
|
||||||
}, openReplyThreadOriginalMessage: { _ in
|
}, openReplyThreadOriginalMessage: { _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
|
@ -1959,7 +1959,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
}, greetingStickerNode: {
|
}, greetingStickerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, openPeerContextMenu: { _, _, _, _ in
|
}, openPeerContextMenu: { _, _, _, _ in
|
||||||
}, openMessageReplies: { _, _ in
|
}, openMessageReplies: { _, _, _ in
|
||||||
}, openReplyThreadOriginalMessage: { _ in
|
}, openReplyThreadOriginalMessage: { _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
@ -5946,7 +5946,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
|
|||||||
private var previousBackButtonBadge: ASDisplayNode?
|
private var previousBackButtonBadge: ASDisplayNode?
|
||||||
private var currentBackButton: ASDisplayNode?
|
private var currentBackButton: ASDisplayNode?
|
||||||
|
|
||||||
private var previousTitleNode: (ASDisplayNode, TextNode)?
|
private var previousTitleNode: (ASDisplayNode, ASDisplayNode)?
|
||||||
private var previousStatusNode: (ASDisplayNode, ASDisplayNode)?
|
private var previousStatusNode: (ASDisplayNode, ASDisplayNode)?
|
||||||
|
|
||||||
private var didSetup: Bool = false
|
private var didSetup: Bool = false
|
||||||
|
@ -1198,7 +1198,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
}, greetingStickerNode: {
|
}, greetingStickerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, openPeerContextMenu: { _, _, _, _ in
|
}, openPeerContextMenu: { _, _, _, _ in
|
||||||
}, openMessageReplies: { _, _ in
|
}, openMessageReplies: { _, _, _ in
|
||||||
}, openReplyThreadOriginalMessage: { _ in
|
}, openReplyThreadOriginalMessage: { _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, 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_Send_ConfirmationConfirm: String { return self._s[218]! }
|
||||||
public var Wallet_Created_ExportErrorTitle: String { return self._s[219]! }
|
public var Wallet_Created_ExportErrorTitle: String { return self._s[219]! }
|
||||||
public var Wallet_Info_TransactionPendingHeader: String { return self._s[220]! }
|
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 form = getPluralizationForm(self.lc, value)
|
||||||
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
||||||
return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue)
|
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 form = getPluralizationForm(self.lc, value)
|
||||||
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
||||||
return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue)
|
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