Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2020-09-25 19:12:11 +03:00
commit 177f91c199
66 changed files with 5905 additions and 4808 deletions

View file

@ -5777,8 +5777,21 @@ Any member of this group will be able to see messages in the channel.";
"Conversation.ContextViewThread" = "View Thread";
"Conversation.ViewReply" = "View Reply";
"Conversation.MessageViewComments_1" = "%@ Comment";
"Conversation.MessageViewComments_any" = "%@ Comments";
"Conversation.MessageViewComments_1" = "[%@]Comment";
"Conversation.MessageViewComments_any" = "[%@]Comments";
"Conversation.MessageViewCommentsFormat" = "%1$@ %2$@";
"Conversation.TitleCommentsEmpty" = "Comments";
"Conversation.TitleComments_1" = "[%@]Comment";
"Conversation.TitleComments_any" = "[%@]Comments";
"Conversation.TitleCommentsFormat" = "%1$@ %2$@";
"Conversation.TitleRepliesEmpty" = "Replies";
"Conversation.TitleReplies_1" = "[%@]Comment";
"Conversation.TitleReplies_any" = "[%@]Comments";
"Conversation.TitleRepliesFormat" = "%1$@ %2$@";
"Conversation.MessageLeaveComment" = "Leave a Comment";
"Conversation.MessageLeaveCommentShort" = "Comment";
@ -5788,10 +5801,12 @@ Any member of this group will be able to see messages in the channel.";
"Conversation.InputTextPlaceholderReply" = "Reply";
"Conversation.InputTextPlaceholderComment" = "Comment";
"Conversation.TitleComments_1" = "%@ Comment";
"Conversation.TitleComments_any" = "%@ Comments";
"Conversation.TitleNoComments" = "Comments";
"Conversation.ContextMenuBlock" = "Block User";
"Replies.BlockAndDeleteRepliesActionTitle" = "Block and Delete Replies";
"Channel.CommentsGroup.Header" = "Select a group chat that will be used to host comments from your channel.";
"Channel.CommentsGroup.HeaderSet" = "%@ is selected as the group that will be used to host comments for your channel.";
"Channel.CommentsGroup.HeaderGroupSet" = "%@ is linking the group as it's discussion board.";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3654,7 +3654,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
}
}
private func updateVisibleItemRange(force: Bool = false) {
public func updateVisibleItemRange(force: Bool = false) {
let currentRange = self.immediateDisplayedItemRange()
if currentRange != self.displayedItemRange || force {

View file

@ -59,7 +59,7 @@ public enum LegacyAttachmentMenuMediaEditing {
case file
}
public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, presentationData: PresentationData, parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: String, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController {
public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocation: ChatLocation, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, presentationData: PresentationData, parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: String, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController {
let defaultVideoPreset = defaultVideoPresetForContext(context)
UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0")
@ -119,7 +119,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaO
let carouselItem = TGAttachmentCarouselItemView(context: parentController.context, camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType, saveEditedPhotos: !isSecretChat && saveEditedPhotos, allowGrouping: editMediaOptions == nil && allowGrouping, allowSelection: editMediaOptions == nil, allowEditing: true, document: false, selectionLimit: selectionLimit)!
carouselItemView = carouselItem
carouselItem.stickersContext = paintStickersContext
carouselItem.suggestionContext = legacySuggestionContext(context: context, peerId: peer.id)
carouselItem.suggestionContext = legacySuggestionContext(context: context, peerId: peer.id, chatLocation: chatLocation)
carouselItem.recipientName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
carouselItem.cameraPressed = { [weak controller, weak parentController] cameraView in
if let controller = controller {
@ -367,7 +367,7 @@ public func legacyMenuPaletteFromTheme(_ theme: PresentationTheme) -> TGMenuShee
return TGMenuSheetPallete(dark: theme.overallDarkAppearance, backgroundColor: sheetTheme.opaqueItemBackgroundColor, selectionColor: sheetTheme.opaqueItemHighlightedBackgroundColor, separatorColor: sheetTheme.opaqueItemSeparatorColor, accentColor: sheetTheme.controlAccentColor, destructiveColor: sheetTheme.destructiveActionTextColor, textColor: sheetTheme.primaryTextColor, secondaryTextColor: sheetTheme.secondaryTextColor, spinnerColor: sheetTheme.secondaryTextColor, badgeTextColor: sheetTheme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: sheetTheme.opaqueItemBackgroundColor))
}
public func presentLegacyPasteMenu(context: AccountContext, peer: Peer, saveEditedPhotos: Bool, allowGrouping: Bool, presentationData: PresentationData, images: [UIImage], sendMessagesWithSignals: @escaping ([Any]?) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, present: (ViewController, Any?) -> Void, initialLayout: ContainerViewLayout? = nil) -> ViewController {
public func presentLegacyPasteMenu(context: AccountContext, peer: Peer, chatLocation: ChatLocation, saveEditedPhotos: Bool, allowGrouping: Bool, presentationData: PresentationData, images: [UIImage], sendMessagesWithSignals: @escaping ([Any]?) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, present: (ViewController, Any?) -> Void, initialLayout: ContainerViewLayout? = nil) -> ViewController {
let defaultVideoPreset = defaultVideoPresetForContext(context)
UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0")
@ -394,7 +394,7 @@ public func presentLegacyPasteMenu(context: AccountContext, peer: Peer, saveEdit
legacyController.enableSizeClassSignal = true
let suggestionContext = legacySuggestionContext(context: context, peerId: peer.id)
let suggestionContext = legacySuggestionContext(context: context, peerId: peer.id, chatLocation: chatLocation)
let paintStickersContext = LegacyPaintStickersContext(context: context)
paintStickersContext.presentStickersController = { completion in

View file

@ -19,7 +19,7 @@ public func guessMimeTypeByFileExtension(_ ext: String) -> String {
return TGMimeTypeMap.mimeType(forExtension: ext) ?? "application/binary"
}
public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: Peer, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: String, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?) {
public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: Peer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: String, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?) {
let isSecretChat = peer.id.namespace == Namespaces.Peer.SecretChat
let paintStickersContext = LegacyPaintStickersContext(context: context)
@ -34,7 +34,7 @@ public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, co
controller.captionsEnabled = captionsEnabled
controller.inhibitDocumentCaptions = false
controller.stickersContext = paintStickersContext
controller.suggestionContext = legacySuggestionContext(context: context, peerId: peer.id)
controller.suggestionContext = legacySuggestionContext(context: context, peerId: peer.id, chatLocation: chatLocation)
if peer.id != context.account.peerId {
if peer is TelegramUser {
controller.hasTimer = hasSchedule

View file

@ -9,13 +9,12 @@ import LegacyUI
import SearchPeerMembers
import AccountContext
public func legacySuggestionContext(context: AccountContext, peerId: PeerId) -> TGSuggestionContext {
public func legacySuggestionContext(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation) -> TGSuggestionContext {
let suggestionContext = TGSuggestionContext()
suggestionContext.userListSignal = { query in
return SSignal { subscriber in
if let query = query {
let normalizedQuery = query.lowercased()
let disposable = searchPeerMembers(context: context, peerId: peerId, query: query).start(next: { peers in
let disposable = searchPeerMembers(context: context, peerId: peerId, chatLocation: chatLocation, query: query).start(next: { peers in
let users = NSMutableArray()
for peer in peers {
let user = TGUser()

View file

@ -31,6 +31,9 @@ public extension Peer {
func displayTitle(strings: PresentationStrings, displayOrder: PresentationPersonNameOrder) -> String {
switch self {
case let user as TelegramUser:
if user.id.isReplies {
return strings.DialogList_Replies
}
if let firstName = user.firstName, !firstName.isEmpty {
if let lastName = user.lastName, !lastName.isEmpty {
switch displayOrder {

View file

@ -136,12 +136,12 @@ private enum ChannelDiscussionGroupSetupControllerEntry: ItemListNodeEntry {
let text: String
if let title = title {
if isGroup {
text = presentationData.strings.Channel_DiscussionGroup_HeaderGroupSet(title).0
text = presentationData.strings.Channel_CommentsGroup_HeaderGroupSet(title).0
} else {
text = presentationData.strings.Channel_DiscussionGroup_HeaderSet(title).0
text = presentationData.strings.Channel_CommentsGroup_HeaderSet(title).0
}
} else {
text = ""
text = presentationData.strings.Channel_CommentsGroup_Header
}
return ChatListFilterSettingsHeaderItem(theme: presentationData.theme, text: text, animation: .discussionGroupSetup, sectionId: self.section)
case let .create(theme, text):

View file

@ -132,9 +132,9 @@ class ChannelDiscussionGroupSetupHeaderItemNode: ListViewItemNode {
let bold = MarkdownAttributeSet(font: titleBoldFont, textColor: item.theme.list.sectionHeaderTextColor)
let string: NSAttributedString
if let title = item.title {
string = addAttributesToStringWithRanges(item.isGroup ? item.strings.Channel_DiscussionGroup_HeaderGroupSet(title) : item.strings.Channel_DiscussionGroup_HeaderSet(title), body: body, argumentAttributes: [0: bold])
string = addAttributesToStringWithRanges(item.isGroup ? item.strings.Channel_CommentsGroup_HeaderGroupSet(title) : item.strings.Channel_CommentsGroup_HeaderSet(title), body: body, argumentAttributes: [0: bold])
} else {
string = NSAttributedString(string: item.strings.Channel_DiscussionGroup_Header, font: titleFont, textColor: item.theme.list.sectionHeaderTextColor)
string = NSAttributedString(string: item.strings.Channel_CommentsGroup_Header, font: titleFont, textColor: item.theme.list.sectionHeaderTextColor)
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))

View file

@ -757,7 +757,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: true)!
let _ = currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { assetsController in
let controller = WebSearchController(context: context, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { result in
let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { result in
assetsController?.dismiss()
completedImpl(result)
}))

View file

@ -1482,7 +1482,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: true)!
let _ = currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { assetsController in
let controller = WebSearchController(context: context, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { result in
let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { result in
assetsController?.dismiss()
completedImpl(result)
}))

View file

@ -3,7 +3,7 @@ import SwiftSignalKit
public enum ChatLocationInput {
case peer(PeerId)
case external(PeerId, Signal<MessageHistoryViewExternalInput, NoError>)
case external(PeerId, Int64, Signal<MessageHistoryViewExternalInput, NoError>)
}
public enum ResolvedChatLocationInput {

View file

@ -2416,7 +2416,7 @@ public final class Postbox {
switch chatLocation {
case let .peer(peerId):
return .single((.peer(peerId), false))
case let .external(_, input):
case let .external(_, _, input):
return Signal { subscriber in
var isHoleFill = false
return (input

View file

@ -5,8 +5,10 @@ import SyncCore
import SwiftSignalKit
import AccountContext
public func searchPeerMembers(context: AccountContext, peerId: PeerId, query: String) -> Signal<[Peer], NoError> {
if peerId.namespace == Namespaces.Peer.CloudChannel {
public func searchPeerMembers(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, query: String) -> Signal<[Peer], NoError> {
if case .replyThread = chatLocation {
return .single([])
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
return context.account.postbox.transaction { transaction -> CachedChannelData? in
return transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData
}

View file

@ -1400,7 +1400,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos, hasViewButton: false, personalPhoto: true, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)!
let _ = currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { assetsController in
let controller = WebSearchController(context: context, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: nil, completion: { result in
let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: nil, completion: { result in
assetsController?.dismiss()
completedProfilePhotoImpl(result)
}))

View file

@ -701,9 +701,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1449145777] = { return Api.upload.CdnFile.parse_cdnFile($0) }
dict[1984136919] = { return Api.wallet.LiteResponse.parse_liteResponse($0) }
dict[415997816] = { return Api.help.InviteText.parse_inviteText($0) }
dict[1984755728] = { return Api.BotInlineMessage.parse_botInlineMessageMediaAuto($0) }
dict[-1937807902] = { return Api.BotInlineMessage.parse_botInlineMessageText($0) }
dict[-1222451611] = { return Api.BotInlineMessage.parse_botInlineMessageMediaGeo($0) }
dict[1984755728] = { return Api.BotInlineMessage.parse_botInlineMessageMediaAuto($0) }
dict[-1970903652] = { return Api.BotInlineMessage.parse_botInlineMessageMediaVenue($0) }
dict[416402882] = { return Api.BotInlineMessage.parse_botInlineMessageMediaContact($0) }
dict[-1673717362] = { return Api.InputPeerNotifySettings.parse_inputPeerNotifySettings($0) }

View file

@ -19856,27 +19856,14 @@ public extension Api {
}
public enum BotInlineMessage: TypeConstructorDescription {
case botInlineMessageMediaAuto(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?)
case botInlineMessageText(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?)
case botInlineMessageMediaGeo(flags: Int32, geo: Api.GeoPoint, period: Int32, replyMarkup: Api.ReplyMarkup?)
case botInlineMessageMediaAuto(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?)
case botInlineMessageMediaVenue(flags: Int32, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String, replyMarkup: Api.ReplyMarkup?)
case botInlineMessageMediaContact(flags: Int32, phoneNumber: String, firstName: String, lastName: String, vcard: String, replyMarkup: Api.ReplyMarkup?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .botInlineMessageMediaAuto(let flags, let message, let entities, let replyMarkup):
if boxed {
buffer.appendInt32(1984755728)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(message, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(entities!.count))
for item in entities! {
item.serialize(buffer, true)
}}
if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)}
break
case .botInlineMessageText(let flags, let message, let entities, let replyMarkup):
if boxed {
buffer.appendInt32(-1937807902)
@ -19899,6 +19886,19 @@ public extension Api {
serializeInt32(period, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)}
break
case .botInlineMessageMediaAuto(let flags, let message, let entities, let replyMarkup):
if boxed {
buffer.appendInt32(1984755728)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(message, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(entities!.count))
for item in entities! {
item.serialize(buffer, true)
}}
if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)}
break
case .botInlineMessageMediaVenue(let flags, let geo, let title, let address, let provider, let venueId, let venueType, let replyMarkup):
if boxed {
buffer.appendInt32(-1970903652)
@ -19928,12 +19928,12 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .botInlineMessageMediaAuto(let flags, let message, let entities, let replyMarkup):
return ("botInlineMessageMediaAuto", [("flags", flags), ("message", message), ("entities", entities), ("replyMarkup", replyMarkup)])
case .botInlineMessageText(let flags, let message, let entities, let replyMarkup):
return ("botInlineMessageText", [("flags", flags), ("message", message), ("entities", entities), ("replyMarkup", replyMarkup)])
case .botInlineMessageMediaGeo(let flags, let geo, let period, let replyMarkup):
return ("botInlineMessageMediaGeo", [("flags", flags), ("geo", geo), ("period", period), ("replyMarkup", replyMarkup)])
case .botInlineMessageMediaAuto(let flags, let message, let entities, let replyMarkup):
return ("botInlineMessageMediaAuto", [("flags", flags), ("message", message), ("entities", entities), ("replyMarkup", replyMarkup)])
case .botInlineMessageMediaVenue(let flags, let geo, let title, let address, let provider, let venueId, let venueType, let replyMarkup):
return ("botInlineMessageMediaVenue", [("flags", flags), ("geo", geo), ("title", title), ("address", address), ("provider", provider), ("venueId", venueId), ("venueType", venueType), ("replyMarkup", replyMarkup)])
case .botInlineMessageMediaContact(let flags, let phoneNumber, let firstName, let lastName, let vcard, let replyMarkup):
@ -19941,30 +19941,6 @@ public extension Api {
}
}
public static func parse_botInlineMessageMediaAuto(_ reader: BufferReader) -> BotInlineMessage? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: [Api.MessageEntity]?
if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self)
} }
var _4: Api.ReplyMarkup?
if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.BotInlineMessage.botInlineMessageMediaAuto(flags: _1!, message: _2!, entities: _3, replyMarkup: _4)
}
else {
return nil
}
}
public static func parse_botInlineMessageText(_ reader: BufferReader) -> BotInlineMessage? {
var _1: Int32?
_1 = reader.readInt32()
@ -20013,6 +19989,30 @@ public extension Api {
return nil
}
}
public static func parse_botInlineMessageMediaAuto(_ reader: BufferReader) -> BotInlineMessage? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: [Api.MessageEntity]?
if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self)
} }
var _4: Api.ReplyMarkup?
if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.BotInlineMessage.botInlineMessageMediaAuto(flags: _1!, message: _2!, entities: _3, replyMarkup: _4)
}
else {
return nil
}
}
public static func parse_botInlineMessageMediaVenue(_ reader: BufferReader) -> BotInlineMessage? {
var _1: Int32?
_1 = reader.readInt32()

View file

@ -5235,6 +5235,21 @@ public extension Api {
return result
})
}
public static func blockFromReplies(flags: Int32, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(698914348)
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(msgId, buffer: buffer, boxed: false)
return (FunctionDescription(name: "contacts.blockFromReplies", parameters: [("flags", flags), ("msgId", msgId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Updates
}
return result
})
}
}
public struct help {
public static func getConfig() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Config>) {

View file

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

View file

@ -190,7 +190,7 @@ private func wrappedHistoryViewAdditionalData(chatLocation: ChatLocationInput, a
result.append(.peerChatState(peerId))
}
}
case let .external(peerId, _):
case let .external(peerId, _, _):
if peerId.namespace == Namespaces.Peer.CloudChannel {
if result.firstIndex(where: { if case .peerChatState = $0 { return true } else { return false } }) == nil {
result.append(.peerChatState(peerId))
@ -716,17 +716,12 @@ public final class AccountViewTracker {
repliesReadMaxId = readMaxId
}
}
var maxReadIncomingMessageId: MessageId?
var maxMessageId: MessageId?
if let commentsChannelId = commentsChannelId {
if let repliesReadMaxId = repliesReadMaxId {
maxReadIncomingMessageId = MessageId(peerId: commentsChannelId, namespace: Namespaces.Message.Cloud, id: repliesReadMaxId)
}
if let repliesMaxId = repliesMaxId {
maxMessageId = MessageId(peerId: commentsChannelId, namespace: Namespaces.Message.Cloud, id: repliesMaxId)
}
}
resultStates[messageIds[i]] = ViewCountContextState(timestamp: Int32(CFAbsoluteTimeGetCurrent()), clientId: clientId, result: ViewCountContextState.ReplyInfo(commentsPeerId: commentsChannelId, maxReadIncomingMessageId: maxReadIncomingMessageId, maxMessageId: maxMessageId))
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ViewCountMessageAttribute {
if let views = views {
@ -736,13 +731,30 @@ public final class AccountViewTracker {
if let forwards = forwards {
attributes[j] = ForwardCountMessageAttribute(count: Int(forwards))
}
} else if let _ = attributes[j] as? ReplyThreadMessageAttribute {
} else if let attribute = attributes[j] as? ReplyThreadMessageAttribute {
foundReplies = true
if let repliesCount = repliesCount {
attributes[j] = ReplyThreadMessageAttribute(count: repliesCount, latestUsers: recentRepliersPeerIds ?? [], commentsPeerId: commentsChannelId, maxMessageId: repliesMaxId, maxReadMessageId: repliesReadMaxId)
var resolvedMaxReadMessageId: MessageId.Id?
if let previousMaxReadMessageId = attribute.maxReadMessageId, let repliesReadMaxIdValue = repliesReadMaxId {
resolvedMaxReadMessageId = max(previousMaxReadMessageId, repliesReadMaxIdValue)
repliesReadMaxId = resolvedMaxReadMessageId
} else if let repliesReadMaxIdValue = repliesReadMaxId {
resolvedMaxReadMessageId = repliesReadMaxIdValue
repliesReadMaxId = resolvedMaxReadMessageId
} else {
resolvedMaxReadMessageId = attribute.maxReadMessageId
}
attributes[j] = ReplyThreadMessageAttribute(count: repliesCount, latestUsers: recentRepliersPeerIds ?? [], commentsPeerId: commentsChannelId, maxMessageId: repliesMaxId, maxReadMessageId: resolvedMaxReadMessageId)
}
}
}
var maxReadIncomingMessageId: MessageId?
if let commentsChannelId = commentsChannelId {
if let repliesReadMaxId = repliesReadMaxId {
maxReadIncomingMessageId = MessageId(peerId: commentsChannelId, namespace: Namespaces.Message.Cloud, id: repliesReadMaxId)
}
}
resultStates[messageIds[i]] = ViewCountContextState(timestamp: Int32(CFAbsoluteTimeGetCurrent()), clientId: clientId, result: ViewCountContextState.ReplyInfo(commentsPeerId: commentsChannelId, maxReadIncomingMessageId: maxReadIncomingMessageId, maxMessageId: maxMessageId))
if !foundReplies, let repliesCount = repliesCount {
attributes.append(ReplyThreadMessageAttribute(count: repliesCount, latestUsers: recentRepliersPeerIds ?? [], commentsPeerId: commentsChannelId, maxMessageId: repliesMaxId, maxReadMessageId: repliesReadMaxId))
}
@ -1229,6 +1241,8 @@ public final class AccountViewTracker {
strongSelf.updatePolls(viewId: viewId, messageIds: pollMessageIds, messages: pollMessageDict)
if case let .peer(peerId) = chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel {
strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0)
} else if case let .external(peerId, _, _) = chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel {
strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0, location: chatLocation)
}
}
}
@ -1242,9 +1256,9 @@ public final class AccountViewTracker {
if peerId.namespace == Namespaces.Peer.CloudChannel {
strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil)
}
case let .external(peerId, _):
case let .external(peerId, _, _):
if peerId.namespace == Namespaces.Peer.CloudChannel {
strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil)
strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil, location: chatLocation)
}
}
}
@ -1255,7 +1269,7 @@ public final class AccountViewTracker {
switch chatLocation {
case let .peer(peerIdValue):
peerId = peerIdValue
case let .external(peerIdValue, _):
case let .external(peerIdValue, _, _):
peerId = peerIdValue
}
if peerId.namespace == Namespaces.Peer.CloudChannel {

View file

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

View file

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

View file

@ -146,6 +146,7 @@ final class HistoryViewStateValidationContexts {
}
return
}
var historyState: HistoryState?
for entry in view.additionalData {
if case let .peerChatState(peerId, chatState) = entry {
@ -156,7 +157,101 @@ final class HistoryViewStateValidationContexts {
}
}
if let historyState = historyState, historyState.hasInvalidationIndex {
if let location = location, case let .external(peerId, threadId, _) = location {
var rangesToInvalidate: [[MessageId]] = []
let addToRange: (MessageId, inout [[MessageId]]) -> Void = { id, ranges in
if ranges.isEmpty {
ranges = [[id]]
} else {
ranges[ranges.count - 1].append(id)
}
}
let addRangeBreak: (inout [[MessageId]]) -> Void = { ranges in
if ranges.last?.count != 0 {
ranges.append([])
}
}
for entry in view.entries {
if entry.message.id.peerId == peerId && entry.message.id.namespace == Namespaces.Message.Cloud {
addToRange(entry.message.id, &rangesToInvalidate)
}
}
if !rangesToInvalidate.isEmpty && rangesToInvalidate[rangesToInvalidate.count - 1].isEmpty {
rangesToInvalidate.removeLast()
}
var invalidatedMessageIds = Set<MessageId>()
if !rangesToInvalidate.isEmpty {
let context: HistoryStateValidationContext
if let current = self.contexts[id] {
context = current
} else {
context = HistoryStateValidationContext()
self.contexts[id] = context
}
var addedRanges: [[MessageId]] = []
for messages in rangesToInvalidate {
for id in messages {
invalidatedMessageIds.insert(id)
if context.batchReferences[id] != nil {
addRangeBreak(&addedRanges)
} else {
addToRange(id, &addedRanges)
}
}
addRangeBreak(&addedRanges)
}
if !addedRanges.isEmpty && addedRanges[addedRanges.count - 1].isEmpty {
addedRanges.removeLast()
}
for rangeMessages in addedRanges {
for messages in slicedForValidationMessages(rangeMessages) {
let disposable = MetaDisposable()
let batch = HistoryStateValidationBatch(disposable: disposable, invalidatedState: historyState)
for messageId in messages {
context.batchReferences[messageId] = batch
}
disposable.set((validateReplyThreadMessagesBatch(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: peerId, threadMessageId: makeThreadIdMessageId(peerId: peerId, threadId: threadId).id, messageIds: messages)
|> deliverOn(self.queue)).start(completed: { [weak self, weak batch] in
if let strongSelf = self, let context = strongSelf.contexts[id], let batch = batch {
var completedMessageIds: [MessageId] = []
for (messageId, messageBatch) in context.batchReferences {
if messageBatch === batch {
completedMessageIds.append(messageId)
}
}
for messageId in completedMessageIds {
//context.batchReferences.removeValue(forKey: messageId)
}
}
}))
}
}
}
if let context = self.contexts[id] {
var removeIds: [MessageId] = []
for batchMessageId in context.batchReferences.keys {
if !invalidatedMessageIds.contains(batchMessageId) {
removeIds.append(batchMessageId)
}
}
for messageId in removeIds {
context.batchReferences.removeValue(forKey: messageId)
}
}
} else if let historyState = historyState, historyState.hasInvalidationIndex {
var rangesToInvalidate: [[MessageId]] = []
let addToRange: (MessageId, inout [[MessageId]]) -> Void = { id, ranges in
if ranges.isEmpty {
@ -421,6 +516,59 @@ private func validateChannelMessagesBatch(postbox: Postbox, network: Network, ac
} |> switchToLatest
}
private func validateReplyThreadMessagesBatch(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, threadMessageId: Int32, messageIds: [MessageId]) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var previousMessages: [Message] = []
var previous: [MessageId: Message] = [:]
for messageId in messageIds {
if let message = transaction.getMessage(messageId) {
previousMessages.append(message)
previous[message.id] = message
}
}
var signal: Signal<ValidatedMessages, MTRpcError>
let hash = hashForMessages(previousMessages, withChannelIds: false)
Logger.shared.log("HistoryValidation", "validate reply thread batch for \(peerId): \(previousMessages.map({ $0.id }))")
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
let requestSignal: Signal<Api.messages.Messages, MTRpcError>
requestSignal = network.request(Api.functions.messages.getReplies(peer: inputPeer, msgId: threadMessageId, offsetId: messageIds[messageIds.count - 1].id, offsetDate: 0, addOffset: -1, limit: Int32(messageIds.count), maxId: messageIds[messageIds.count - 1].id + 1, minId: messageIds[0].id - 1, hash: hash))
signal = requestSignal
|> map { result -> ValidatedMessages in
let messages: [Api.Message]
let chats: [Api.Chat]
let users: [Api.User]
var channelPts: Int32?
switch result {
case let .messages(messages: apiMessages, chats: apiChats, users: apiUsers):
messages = apiMessages
chats = apiChats
users = apiUsers
case let .messagesSlice(_, _, _, messages: apiMessages, chats: apiChats, users: apiUsers):
messages = apiMessages
chats = apiChats
users = apiUsers
case let .channelMessages(_, pts, _, apiMessages, apiChats, apiUsers):
messages = apiMessages
chats = apiChats
users = apiUsers
channelPts = pts
case .messagesNotModified:
return .notModified
}
return .messages(messages, chats, users, channelPts)
}
} else {
return .complete()
}
return validateReplyThreadBatch(postbox: postbox, network: network, transaction: transaction, accountPeerId: accountPeerId, peerId: peerId, threadId: makeMessageThreadId(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: threadMessageId)), signal: signal, previous: previous, messageNamespace: Namespaces.Message.Cloud)
}
|> switchToLatest
}
private func validateScheduledMessagesBatch(postbox: Postbox, network: Network, accountPeerId: PeerId, tag: MessageTags?, messages: [Message], historyState: HistoryState) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var signal: Signal<ValidatedMessages, MTRpcError>
@ -715,3 +863,154 @@ private func validateBatch(postbox: Postbox, network: Network, transaction: Tran
}
}
}
private func validateReplyThreadBatch(postbox: Postbox, network: Network, transaction: Transaction, accountPeerId: PeerId, peerId: PeerId, threadId: Int64, signal: Signal<ValidatedMessages, MTRpcError>, previous: [MessageId: Message], messageNamespace: MessageId.Namespace) -> Signal<Void, NoError> {
return signal
|> map(Optional.init)
|> `catch` { _ -> Signal<ValidatedMessages?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
guard let result = result else {
return .complete()
}
switch result {
case let .messages(messages, _, _, channelPts):
var storeMessages: [StoreMessage] = []
for message in messages {
if let storeMessage = StoreMessage(apiMessage: message, namespace: messageNamespace) {
var attributes = storeMessage.attributes
if let channelPts = channelPts {
attributes.append(ChannelMessageStateVersionAttribute(pts: channelPts))
}
storeMessages.append(storeMessage.withUpdatedAttributes(attributes))
}
}
var validMessageIds = Set<MessageId>()
for message in storeMessages {
if case let .Id(id) = message.id {
validMessageIds.insert(id)
}
}
var maybeRemovedMessageIds: [MessageId] = []
for id in previous.keys {
if !validMessageIds.contains(id) {
maybeRemovedMessageIds.append(id)
}
}
let actuallyRemovedMessagesSignal: Signal<Set<MessageId>, NoError>
if maybeRemovedMessageIds.isEmpty {
actuallyRemovedMessagesSignal = .single(Set())
} else {
actuallyRemovedMessagesSignal = postbox.transaction { transaction -> Signal<Set<MessageId>, NoError> in
if let inputChannel = transaction.getPeer(peerId).flatMap(apiInputChannel) {
return network.request(Api.functions.channels.getMessages(channel: inputChannel, id: maybeRemovedMessageIds.map({ Api.InputMessage.inputMessageID(id: $0.id) })))
|> map { result -> Set<MessageId> in
let apiMessages: [Api.Message]
switch result {
case let .channelMessages(_, _, _, messages, _, _):
apiMessages = messages
case let .messages(messages, _, _):
apiMessages = messages
case let .messagesSlice(_, _, _, messages, _, _):
apiMessages = messages
case .messagesNotModified:
return Set()
}
var ids = Set<MessageId>()
for message in apiMessages {
if let parsedMessage = StoreMessage(apiMessage: message, namespace: messageNamespace), case let .Id(id) = parsedMessage.id {
ids.insert(id)
}
}
return Set(maybeRemovedMessageIds).subtracting(ids)
}
|> `catch` { _ -> Signal<Set<MessageId>, NoError> in
return .single(Set(maybeRemovedMessageIds))
}
}
return .single(Set(maybeRemovedMessageIds))
}
|> switchToLatest
}
return actuallyRemovedMessagesSignal
|> mapToSignal { removedMessageIds -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
var validMessageIds = Set<MessageId>()
for message in storeMessages {
if case let .Id(id) = message.id {
validMessageIds.insert(id)
let previousMessage = previous[id] ?? transaction.getMessage(id)
if let previousMessage = previousMessage {
var updatedTimestamp = message.timestamp
inner: for attribute in message.attributes {
if let attribute = attribute as? EditedMessageAttribute {
updatedTimestamp = attribute.date
break inner
}
}
var timestamp = previousMessage.timestamp
inner: for attribute in previousMessage.attributes {
if let attribute = attribute as? EditedMessageAttribute {
timestamp = attribute.date
break inner
}
}
transaction.updateMessage(id, update: { currentMessage in
if updatedTimestamp != timestamp {
var updatedLocalTags = message.localTags
if currentMessage.localTags.contains(.OutgoingLiveLocation) {
updatedLocalTags.insert(.OutgoingLiveLocation)
}
return .update(message.withUpdatedLocalTags(updatedLocalTags))
} else {
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
}
var attributes = currentMessage.attributes
if let channelPts = channelPts {
for i in (0 ..< attributes.count).reversed() {
if let _ = attributes[i] as? ChannelMessageStateVersionAttribute {
attributes.remove(at: i)
}
}
attributes.append(ChannelMessageStateVersionAttribute(pts: channelPts))
}
let updatedFlags = StoreMessageFlags(currentMessage.flags)
return .update(StoreMessage(id: message.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: updatedFlags, tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
}
})
if previous[id] == nil {
print("\(id) missing")
}
} else {
let _ = transaction.addMessages([message], location: .Random)
}
}
}
for id in removedMessageIds {
if !validMessageIds.contains(id) {
deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [id])
Logger.shared.log("HistoryValidation", "deleting thread message \(id) in \(id.peerId)")
}
}
}
}
case .notModified:
return .complete()
}
}
}

View file

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

View file

@ -199,3 +199,30 @@ public func dismissPeerStatusOptions(account: Account, peerId: PeerId) -> Signal
}
} |> switchToLatest
}
public func reportRepliesMessage(account: Account, messageId: MessageId, deleteMessage: Bool, deleteHistory: Bool, reportSpam: Bool) -> Signal<Never, NoError> {
if messageId.namespace != Namespaces.Message.Cloud {
return .complete()
}
var flags: Int32 = 0
if deleteMessage {
flags |= 1 << 0
}
if deleteHistory {
flags |= 1 << 1
}
if reportSpam {
flags |= 1 << 2
}
return account.network.request(Api.functions.contacts.blockFromReplies(flags: flags, msgId: messageId.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}

View file

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

View file

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

View file

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

View file

@ -305,7 +305,7 @@ public final class AccountContextImpl: AccountContext {
return .peer(peerId)
case let .replyThread(data):
let context = chatLocationContext(holder: contextHolder, account: self.account, data: data)
return .external(data.messageId.peerId, context.state)
return .external(data.messageId.peerId, makeMessageThreadId(data.messageId), context.state)
}
}

View file

@ -264,11 +264,9 @@ final class AuthorizedApplicationContext {
if UIApplication.shared.applicationState == .active {
var chatIsVisible = false
if let topController = strongSelf.rootController.topViewController as? ChatControllerImpl, topController.traceVisibility() {
if case .peer(firstMessage.id.peerId) = topController.chatLocation {
if topController.chatLocation.peerId == firstMessage.id.peerId {
chatIsVisible = true
}/* else if case let .group(topGroupId) = topController.chatLocation, topGroupId == groupId {
chatIsVisible = true
}*/
}
}
if !notify {

View file

@ -178,6 +178,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var didSetChatLocationInfoReady = false
private let chatLocationInfoData: ChatLocationInfoData
private let cachedDataReady = Promise<Bool>()
private var didSetCachedDataReady = false
private var presentationInterfaceState: ChatPresentationInterfaceState
private var chatTitleView: ChatTitleView?
@ -289,6 +292,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var checkedPeerChatServiceActions = false
private var willAppear = false
private var didAppear = false
private var scheduledActivateInput = false
@ -2186,12 +2190,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: items, reactionItems: [], gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
})
}, openMessageReplies: { [weak self] messageId, isChannelPost in
}, openMessageReplies: { [weak self] messageId, isChannelPost, displayModalProgress in
guard let strongSelf = self else {
return
}
strongSelf.openMessageReplies(messageId: messageId, isChannelPost: isChannelPost, atMessage: nil)
strongSelf.openMessageReplies(messageId: messageId, isChannelPost: isChannelPost, atMessage: nil, displayModalProgress: displayModalProgress)
}, openReplyThreadOriginalMessage: { [weak self] message in
guard let strongSelf = self else {
return
@ -2206,8 +2210,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
for attribute in message.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
if let threadMessageId = threadMessageId {
if let navigationController = strongSelf.navigationController as? NavigationController {
strongSelf.openMessageReplies(messageId: threadMessageId, isChannelPost: true, atMessage: attribute.messageId)
if let _ = strongSelf.navigationController as? NavigationController {
strongSelf.openMessageReplies(messageId: threadMessageId, isChannelPost: true, atMessage: attribute.messageId, displayModalProgress: true)
}
} else {
strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId))
@ -2673,14 +2677,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
let text: String
if count == 0 {
text = strongSelf.presentationData.strings.Conversation_TitleNoComments
} else {
text = strongSelf.presentationData.strings.Conversation_TitleComments(Int32(count))
}
strongSelf.chatTitleView?.titleContent = .replyThread(type: replyThreadType, text: text)
strongSelf.chatTitleView?.titleContent = .replyThread(type: replyThreadType, count: count)
let firstTime = strongSelf.peerView == nil
strongSelf.peerView = peerView
@ -3264,8 +3261,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if let _ = combinedInitialData.cachedData as? CachedSecretChatData {
}
if case .replyThread = strongSelf.chatLocation {
pinnedMessageId = nil
if case let .replyThread(replyThreadMessageId) = strongSelf.chatLocation {
pinnedMessageId = replyThreadMessageId.messageId
}
var pinnedMessage: Message?
@ -3435,7 +3432,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let callsDataUpdated = strongSelf.presentationInterfaceState.callsAvailable != callsAvailable || strongSelf.presentationInterfaceState.callsPrivate != callsPrivate
if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.pinnedMessage?.stableVersion != pinnedMessage?.stableVersion || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || pinnedMessageUpdated || callsDataUpdated || strongSelf.presentationInterfaceState.slowmodeState != slowmodeState {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in
return state
.updatedPinnedMessageId(pinnedMessageId)
.updatedPinnedMessage(pinnedMessage)
@ -3478,6 +3475,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
.updatedSlowmodeState(slowmodeState)
})
}
if !strongSelf.didSetCachedDataReady {
strongSelf.didSetCachedDataReady = true
strongSelf.cachedDataReady.set(.single(true))
}
}
})
@ -3489,8 +3491,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyState.get(), self._chatLocationInfoReady.get(), initialData) |> map { _, chatLocationInfoReady, _ in
return chatLocationInfoReady
self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyState.get(), self._chatLocationInfoReady.get(), self.cachedDataReady.get(), initialData) |> map { _, chatLocationInfoReady, cachedDataReady, _ in
return chatLocationInfoReady && cachedDataReady
})
if self.context.sharedContext.immediateExperimentalUISettings.crashOnLongQueries {
@ -3957,9 +3959,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let _ = (strongSelf.context.account.postbox.transaction { transasction -> Void in
deleteAllMessagesWithForwardAuthor(transaction: transasction, mediaBox: account.postbox.mediaBox, peerId: message.id.peerId, forwardAuthorId: peer.id, namespace: Namespaces.Message.Cloud)
}).start()
if reportSpam {
let _ = TelegramCore.reportPeer(account: strongSelf.context.account, peerId: peer.id, reason: .spam).start()
}
let _ = reportRepliesMessage(account: strongSelf.context.account, messageId: message.id, deleteMessage: true, deleteHistory: true, reportSpam: reportSpam).start()
})
] as [ActionSheetItem])
@ -5398,6 +5398,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if self.willAppear {
self.chatDisplayNode.historyNode.refreshPollActionsForVisibleMessages()
} else {
self.willAppear = true
}
if self.scheduledActivateInput {
self.scheduledActivateInput = false
@ -6087,6 +6093,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let currentButton = self.rightNavigationButton?.action, currentButton == button.action {
animated = false
}
if case .replyThread = self.chatLocation {
animated = false
}
self.navigationItem.setRightBarButton(button.buttonItem, animated: animated)
self.rightNavigationButton = button
}
@ -6703,7 +6712,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
slowModeEnabled = true
}
let controller = legacyAttachmentMenu(context: strongSelf.context, peer: peer, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: !strongSelf.presentationInterfaceState.isScheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, canSendPolls: canSendPolls, presentationData: strongSelf.presentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText.string, openGallery: {
let controller = legacyAttachmentMenu(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: !strongSelf.presentationInterfaceState.isScheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, canSendPolls: canSendPolls, presentationData: strongSelf.presentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText.string, openGallery: {
self?.presentMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in
if !inputText.string.isEmpty {
//strongSelf.clearInputText()
@ -6716,7 +6725,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}, openCamera: { [weak self] cameraView, menuController in
if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
presentedLegacyCamera(context: strongSelf.context, peer: peer, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: settings.storeEditedPhotos, mediaGrouping: true, initialCaption: inputText.string, hasSchedule: !strongSelf.presentationInterfaceState.isScheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: settings.storeEditedPhotos, mediaGrouping: true, initialCaption: inputText.string, hasSchedule: !strongSelf.presentationInterfaceState.isScheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
if let strongSelf = self {
if editMediaOptions != nil {
strongSelf.editMessageMediaWithLegacySignals(signals!)
@ -6967,9 +6976,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
legacyController.bind(controller: controller)
legacyController.deferScreenEdgeGestures = [.top]
configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, initialCaption: inputText.string, hasSchedule: !strongSelf.presentationInterfaceState.isScheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in
configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, initialCaption: inputText.string, hasSchedule: !strongSelf.presentationInterfaceState.isScheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in
if let strongSelf = self {
let controller = WebSearchController(context: strongSelf.context, peer: peer, configuration: searchBotsConfiguration, mode: .media(completion: { results, selectionState, editingState, silentPosting in
let controller = WebSearchController(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(completion: { results, selectionState, editingState, silentPosting in
if let legacyController = legacyController {
legacyController.dismiss()
}
@ -7064,7 +7073,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
|> deliverOnMainQueue).start(next: { [weak self] configuration in
if let strongSelf = self {
let controller = WebSearchController(context: strongSelf.context, peer: peer, configuration: configuration, mode: .media(completion: { [weak self] results, selectionState, editingState, silentPosting in
let controller = WebSearchController(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(completion: { [weak self] results, selectionState, editingState, silentPosting in
legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in
if let strongSelf = self {
strongSelf.enqueueChatContextResult(results, result, hideVia: true)
@ -7668,7 +7677,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|> deliverOnMainQueue).start(next: { [weak self] settings in
if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
strongSelf.chatDisplayNode.dismissInput()
let _ = presentLegacyPasteMenu(context: strongSelf.context, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, presentationData: strongSelf.presentationData, images: images, sendMessagesWithSignals: { signals in
let _ = presentLegacyPasteMenu(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, presentationData: strongSelf.presentationData, images: images, sendMessagesWithSignals: { signals in
self?.enqueueMediaMessages(signals: signals, silentPosting: false)
}, presentStickers: { [weak self] completion in
if let strongSelf = self {
@ -8320,21 +8329,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
private func openMessageReplies(messageId: MessageId, isChannelPost: Bool, atMessage atMessageId: MessageId?) {
private func openMessageReplies(messageId: MessageId, isChannelPost: Bool, atMessage atMessageId: MessageId?, displayModalProgress: Bool) {
guard let navigationController = self.navigationController as? NavigationController else {
return
}
if !displayModalProgress, self.controllerInteraction?.currentMessageWithLoadingReplyThread == messageId {
return
}
let progressSignal: Signal<Never, NoError> = Signal { [weak self] _ in
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
return EmptyDisposable
}
let previousId = controllerInteraction.currentMessageWithLoadingReplyThread
controllerInteraction.currentMessageWithLoadingReplyThread = messageId
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(messageId)
if let previousId = previousId {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(previousId)
if !displayModalProgress, controllerInteraction.currentMessageWithLoadingReplyThread != messageId {
let previousId = controllerInteraction.currentMessageWithLoadingReplyThread
controllerInteraction.currentMessageWithLoadingReplyThread = messageId
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(messageId)
if let previousId = previousId {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(previousId)
}
}
return ActionDisposable {
@ -8342,7 +8357,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
return
}
if controllerInteraction.currentMessageWithLoadingReplyThread == messageId {
if !displayModalProgress, controllerInteraction.currentMessageWithLoadingReplyThread == messageId {
controllerInteraction.currentMessageWithLoadingReplyThread = nil
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(messageId)
}
@ -8356,7 +8371,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.navigationActionDisposable.set((ChatControllerImpl.openMessageReplies(context: self.context, navigationController: navigationController, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, messageId: messageId, isChannelPost: isChannelPost, atMessage: atMessageId, displayModalProgress: false)
}, messageId: messageId, isChannelPost: isChannelPost, atMessage: atMessageId, displayModalProgress: displayModalProgress)
|> afterDisposed {
progress.dispose()
}).start())
@ -8386,6 +8401,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let subject: ChatControllerSubject?
if let atMessageId = atMessageId {
subject = .message(atMessageId)
} else if result.scrollToLowerBound {
subject = .message(MessageId(peerId: result.message.messageId.peerId, namespace: Namespaces.Message.Cloud, id: 1))
} else {
subject = nil
}

View file

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

View file

@ -547,6 +547,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let isTopReplyThreadMessageShown = ValuePromise<Bool>(false, ignoreRepeated: true)
private let clientId: Atomic<Int32>
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tagMask: MessageTags?, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal<Set<MessageId>?, NoError>, mode: ChatHistoryListMode = .bubbles) {
self.context = context
self.chatLocation = chatLocation
@ -563,6 +565,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.prefetchManager = InChatPrefetchManager(context: context)
let clientId = Atomic<Int32>(value: nextClientId)
self.clientId = clientId
nextClientId += 1
super.init()
self.dynamicBounceEnabled = !self.currentPresentationData.disableAnimations
@ -570,11 +576,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
//self.debugInfo = true
let clientId = nextClientId
nextClientId += 1
self.messageProcessingManager.process = { [weak context] messageIds in
context?.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds, clientId: clientId)
context?.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds, clientId: clientId.with { $0 })
}
self.messageReactionsProcessingManager.process = { [weak context] messageIds in
context?.account.viewTracker.updateReactionsForMessageIds(messageIds: messageIds)
@ -1098,8 +1101,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.loadStateUpdated = f
}
func refreshPollActionsForVisibleMessages() {
let _ = self.clientId.swap(nextClientId)
nextClientId += 1
self.updateVisibleItemRange(force: true)
}
private func processDisplayedItemRangeChanged(displayedRange: ListViewDisplayedItemRange, transactionState: ChatHistoryTransactionOpaqueState) {
let historyView = transactionState.historyView
var isTopReplyThreadMessageShownValue = false
if let visible = displayedRange.visibleRange {
let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)
if indexRange.0 > indexRange.1 {
@ -1120,8 +1131,6 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
var messagesWithPreloadableMediaToEarlier: [(Message, Media)] = []
var messagesWithPreloadableMediaToLater: [(Message, Media)] = []
var isTopReplyThreadMessageShownValue = false
if indexRange.0 <= indexRange.1 {
for i in (indexRange.0 ... indexRange.1) {
switch historyView.filteredEntries[i] {
@ -1320,9 +1329,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.maxVisibleMessageIndexUpdated?(maxOverallIndex)
}
}
self.isTopReplyThreadMessageShown.set(isTopReplyThreadMessageShownValue)
}
self.isTopReplyThreadMessageShown.set(isTopReplyThreadMessageShownValue)
if let loaded = displayedRange.loadedRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last {
if loaded.firstIndex < 5 && historyView.originalView.laterId != nil {

View file

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

View file

@ -273,7 +273,6 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
var loadCopyMediaResource: MediaResource?
var isAction = false
var diceEmoji: String?
var canDiscuss = false
if messages.count == 1 {
for media in messages[0].media {
if let file = media as? TelegramMediaFile {
@ -309,17 +308,6 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel {
if !isAction {
canPin = channel.hasPermission(.pinMessages)
if messages[0].id.namespace == Namespaces.Message.Cloud {
switch channel.info {
case let .broadcast(info):
if info.flags.contains(.hasDiscussionGroup) {
canDiscuss = true
}
case .group:
break
}
}
}
} else if let group = messages[0].peers[messages[0].id.peerId] as? TelegramGroup {
if !isAction {
@ -602,8 +590,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
}
}
if let threadId = threadId {
let replyThreadId = makeThreadIdMessageId(peerId: messages[0].id.peerId, threadId: threadId)
if let _ = threadId {
let text: String
if threadMessageCount != 0 {
text = chatPresentationInterfaceState.strings.Conversation_ContextViewReplies(Int32(threadMessageCount))
@ -613,37 +600,8 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
actions.append(.action(ContextMenuActionItem(text: text, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replies"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
let foundIndex = Promise<ChatReplyThreadMessage?>()
if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel {
foundIndex.set(fetchChannelReplyThreadMessage(account: context.account, messageId: messages[0].id, atMessageId: nil)
|> map(Optional.init)
|> `catch` { _ -> Signal<ChatReplyThreadMessage?, NoError> in
return .single(nil)
})
}
c.dismiss(completion: {
if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel {
var cancelImpl: (() -> Void)?
let statusController = OverlayStatusController(theme: chatPresentationInterfaceState.theme, type: .loading(cancelled: {
cancelImpl?()
}))
controllerInteraction.presentController(statusController, nil)
let disposable = (foundIndex.get()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak statusController] result in
statusController?.dismiss()
if let result = result {
interfaceInteraction.viewReplies(nil, result)
}
})
cancelImpl = { [weak statusController] in
disposable.dispose()
statusController?.dismiss()
}
}
controllerInteraction.openMessageReplies(messages[0].id, true, true)
})
})))
}

View file

@ -43,7 +43,7 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation
for query in inputQueries {
let previousQuery = currentQueryStates[query.kind]?.0
if previousQuery != query {
let signal = updatedContextQueryResultStateForQuery(context: context, peer: peer, inputQuery: query, previousQuery: previousQuery)
let signal = updatedContextQueryResultStateForQuery(context: context, peer: peer, chatLocation: chatPresentationInterfaceState.chatLocation, inputQuery: query, previousQuery: previousQuery)
updates[query.kind] = .update(query, signal)
}
}
@ -64,7 +64,7 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation
return updates
}
private func updatedContextQueryResultStateForQuery(context: AccountContext, peer: Peer, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
private func updatedContextQueryResultStateForQuery(context: AccountContext, peer: Peer, chatLocation: ChatLocation, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
switch inputQuery {
case let .emoji(query):
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
@ -145,7 +145,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
}
let inlineBots: Signal<[(Peer, Double)], NoError> = types.contains(.contextBots) ? recentlyUsedInlineBots(postbox: context.account.postbox) : .single([])
let participants = combineLatest(inlineBots, searchPeerMembers(context: context, peerId: peer.id, query: query))
let participants = combineLatest(inlineBots, searchPeerMembers(context: context, peerId: peer.id, chatLocation: chatLocation, query: query))
|> map { inlineBots, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
let filteredInlineBots = inlineBots.sorted(by: { $0.1 > $1.1 }).filter { peer, rating in
if rating < 0.14 {
@ -347,7 +347,7 @@ func searchQuerySuggestionResultStateForChatInterfacePresentationState(_ chatPre
}
}
let participants = searchPeerMembers(context: context, peerId: peer.id, query: query)
let participants = searchPeerMembers(context: context, peerId: peer.id, chatLocation: chatPresentationInterfaceState.chatLocation, query: query)
|> map { peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
let filteredPeers = peers
var sortedPeers: [Peer] = []

View file

@ -753,7 +753,7 @@ final class ChatMediaInputNode: ChatInputNode {
let inputNodeInteraction = self.inputNodeInteraction!
let peerSpecificPack: Signal<(PeerSpecificPackData?, CanInstallPeerSpecificPack), NoError>
if let peerId = peerId, case .peer = chatLocation {
if let peerId = peerId {
self.dismissedPeerSpecificStickerPack.set(context.account.postbox.transaction { transaction -> Bool in
guard let state = transaction.getPeerChatInterfaceState(peerId) as? ChatInterfaceState else {
return false

View file

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

View file

@ -1366,7 +1366,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
string = rank.trimmingEmojis
}
adminBadgeString = NSAttributedString(string: " \(string)", font: inlineBotPrefixFont, textColor: messageTheme.secondaryTextColor)
} else if authorIsChannel {
} else if authorIsChannel, case .peer = item.chatLocation {
adminBadgeString = NSAttributedString(string: " \(item.presentationData.strings.Channel_Status)", font: inlineBotPrefixFont, textColor: messageTheme.secondaryTextColor)
}
if let authorNameString = authorNameString, let authorNameColor = authorNameColor, let inlineBotNameString = inlineBotNameString {

View file

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

View file

@ -292,7 +292,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
var ignoreSource = false
if let forwardInfo = item.message.forwardInfo {
if item.message.id.peerId != item.context.account.peerId {
if !item.message.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
for attribute in item.message.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
if attribute.messageId.peerId == forwardInfo.author?.id {
@ -303,6 +303,8 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
break
}
}
} else {
ignoreForward = true
}
}
@ -326,7 +328,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
}
}
if !ignoreSource, item.message.id.peerId != item.context.account.peerId {
if !ignoreSource, !item.message.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
for attribute in item.message.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
if let sourcePeer = item.message.peers[attribute.messageId.peerId] {
@ -746,7 +748,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
for attribute in item.message.attributes {
if let _ = attribute as? ReplyThreadMessageAttribute {
item.controllerInteraction.openMessageReplies(item.message.id, true)
item.controllerInteraction.openMessageReplies(item.message.id, true, false)
return
}
}

View file

@ -115,7 +115,9 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
title = peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
} else if let author = firstMessage.author {
if author.id != peer.id {
if firstMessage.id.peerId.isReplies, let _ = firstMessage.sourceReference, let effectiveAuthor = firstMessage.forwardInfo?.author {
title = effectiveAuthor.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder) + "@" + peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
} else if author.id != peer.id {
if author.id == item.context.account.peerId {
title = presentationData.strings.DialogList_You + "@" + peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
} else {
@ -136,14 +138,18 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
title = peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
}
if let text = title, firstMessage.flags.contains(.WasScheduled) {
if let _ = title, firstMessage.flags.contains(.WasScheduled) {
if let author = firstMessage.author, author.id == peer.id, author.id == item.context.account.peerId {
isReminder = true
} else {
isScheduled = true
}
}
self.avatarNode.setPeer(context: item.context, theme: presentationData.theme, peer: peer, overrideImage: peer.id == item.context.account.peerId ? .savedMessagesIcon : nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor)
var avatarPeer = peer
if firstMessage.id.peerId.isReplies, let author = firstMessage.forwardInfo?.author {
avatarPeer = author
}
self.avatarNode.setPeer(context: item.context, theme: presentationData.theme, peer: avatarPeer, overrideImage: peer.id == item.context.account.peerId ? .savedMessagesIcon : nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor)
}
var titleIcon: UIImage?

View file

@ -235,7 +235,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
switch item.chatLocation {
case let .peer(peerId):
if peerId != item.context.account.peerId {
if !peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
if peerId.isGroupOrChannel && item.message.author != nil {
var isBroadcastChannel = false
if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
@ -814,7 +814,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
for attribute in item.message.attributes {
if let _ = attribute as? ReplyThreadMessageAttribute {
item.controllerInteraction.openMessageReplies(item.message.id, true)
item.controllerInteraction.openMessageReplies(item.message.id, true, false)
return
}
}

View file

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

View file

@ -685,6 +685,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
(.canBanUsers, self.presentationData.strings.Channel_AdminLog_CanBanUsers),
(.canInviteUsers, self.presentationData.strings.Channel_AdminLog_CanInviteUsers),
(.canPinMessages, self.presentationData.strings.Channel_AdminLog_CanPinMessages),
(.canBeAnonymous, self.presentationData.strings.Channel_AdminLog_CanBeAnonymous),
(.canAddAdmins, self.presentationData.strings.Channel_AdminLog_CanAddAdmins)
]
}

View file

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

View file

@ -445,7 +445,7 @@ public func createChannelController(context: AccountContext) -> ViewController {
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)!
let _ = currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { assetsController in
let controller = WebSearchController(context: context, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in
let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in
assetsController?.dismiss()
completedChannelPhotoImpl(result)
}))

View file

@ -704,7 +704,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)!
let _ = currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { assetsController in
let controller = WebSearchController(context: context, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in
let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in
assetsController?.dismiss()
completedGroupPhotoImpl(result)
}))

View file

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

View file

@ -11,7 +11,7 @@ import ShareController
import LegacyUI
import LegacyMediaPickerUI
func presentedLegacyCamera(context: AccountContext, peer: Peer, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: String, hasSchedule: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?) {
func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: String, hasSchedule: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme)
legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait)
@ -79,7 +79,7 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, cameraView: TGAt
controller.allowCaptionEntities = true
controller.allowGrouping = mediaGrouping
controller.inhibitDocumentCaptions = false
controller.suggestionContext = legacySuggestionContext(context: context, peerId: peer.id)
controller.suggestionContext = legacySuggestionContext(context: context, peerId: peer.id, chatLocation: chatLocation)
controller.recipientName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
if peer.id != context.account.peerId {
if peer is TelegramUser {

View file

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

View file

@ -1959,7 +1959,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
@ -4138,7 +4138,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
guard let strongSelf = self else {
return
}
let controller = WebSearchController(context: strongSelf.context, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: strongSelf.isSettings ? nil : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in
let controller = WebSearchController(context: strongSelf.context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: strongSelf.isSettings ? nil : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in
assetsController?.dismiss()
self?.updateProfilePhoto(result)
}))
@ -5940,7 +5940,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
private var previousBackButtonBadge: ASDisplayNode?
private var currentBackButton: ASDisplayNode?
private var previousTitleNode: (ASDisplayNode, TextNode)?
private var previousTitleNode: (ASDisplayNode, ASDisplayNode)?
private var previousStatusNode: (ASDisplayNode, ASDisplayNode)?
private var didSetup: Bool = false

View file

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

View file

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

View file

@ -313,7 +313,7 @@ private func galleryItems(account: Account, results: [ChatContextResult], curren
return (galleryItems, focusItem)
}
func presentLegacyWebSearchGallery(context: AccountContext, peer: Peer?, presentationData: PresentationData, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (ChatContextResult) -> Void, presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?, present: (ViewController, Any?) -> Void) {
func presentLegacyWebSearchGallery(context: AccountContext, peer: Peer?, chatLocation: ChatLocation?, presentationData: PresentationData, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (ChatContextResult) -> Void, presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?, present: (ViewController, Any?) -> Void) {
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
@ -338,8 +338,8 @@ func presentLegacyWebSearchGallery(context: AccountContext, peer: Peer?, present
let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: false, allowCaptionEntities: true, hasTimer: false, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: false, hasCamera: false, recipientName: peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))!
model.stickersContext = paintStickersContext
if let peer = peer {
model.suggestionContext = legacySuggestionContext(context: context, peerId: peer.id)
if let peer = peer, let chatLocation = chatLocation {
model.suggestionContext = legacySuggestionContext(context: context, peerId: peer.id, chatLocation: chatLocation)
}
controller.model = model
model.controller = controller

View file

@ -126,6 +126,7 @@ public final class WebSearchController: ViewController {
private let context: AccountContext
private let mode: WebSearchControllerMode
private let peer: Peer?
private let chatLocation: ChatLocation?
private let configuration: SearchBotsConfiguration
private var controllerNode: WebSearchControllerNode {
@ -155,10 +156,11 @@ public final class WebSearchController: ViewController {
}
}
public init(context: AccountContext, peer: Peer?, configuration: SearchBotsConfiguration, mode: WebSearchControllerMode) {
public init(context: AccountContext, peer: Peer?, chatLocation: ChatLocation?, configuration: SearchBotsConfiguration, mode: WebSearchControllerMode) {
self.context = context
self.mode = mode
self.peer = peer
self.chatLocation = chatLocation
self.configuration = configuration
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -319,7 +321,7 @@ public final class WebSearchController: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = WebSearchControllerNode(context: self.context, presentationData: self.interfaceState.presentationData, controllerInteraction: self.controllerInteraction!, peer: self.peer, mode: self.mode.mode)
self.displayNode = WebSearchControllerNode(context: self.context, presentationData: self.interfaceState.presentationData, controllerInteraction: self.controllerInteraction!, peer: self.peer, chatLocation: self.chatLocation, mode: self.mode.mode)
self.controllerNode.requestUpdateInterfaceState = { [weak self] animated, f in
if let strongSelf = self {
strongSelf.updateInterfaceState(f)

View file

@ -128,6 +128,7 @@ private func preparedWebSearchRecentTransition(from fromEntries: [WebSearchRecen
class WebSearchControllerNode: ASDisplayNode {
private let context: AccountContext
private let peer: Peer?
private let chatLocation: ChatLocation?
private var theme: PresentationTheme
private var strings: PresentationStrings
private var presentationData: PresentationData
@ -180,13 +181,14 @@ class WebSearchControllerNode: ASDisplayNode {
var presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?
init(context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchControllerInteraction, peer: Peer?, mode: WebSearchMode) {
init(context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchControllerInteraction, peer: Peer?, chatLocation: ChatLocation?, mode: WebSearchMode) {
self.context = context
self.theme = presentationData.theme
self.strings = presentationData.strings
self.presentationData = presentationData
self.controllerInteraction = controllerInteraction
self.peer = peer
self.chatLocation = chatLocation
self.mode = mode
self.webSearchInterfaceState = WebSearchInterfaceState(presentationData: context.sharedContext.currentPresentationData.with { $0 })
@ -694,7 +696,7 @@ class WebSearchControllerNode: ASDisplayNode {
if self.controllerInteraction.selectionState != nil {
if let state = self.webSearchInterfaceState.state, state.scope == .images {
if let results = self.currentProcessedResults?.results {
presentLegacyWebSearchGallery(context: self.context, peer: self.peer, presentationData: self.presentationData, results: results, current: currentResult, selectionContext: self.controllerInteraction.selectionState, editingContext: self.controllerInteraction.editingState, updateHiddenMedia: { [weak self] id in
presentLegacyWebSearchGallery(context: self.context, peer: self.peer, chatLocation: self.chatLocation, presentationData: self.presentationData, results: results, current: currentResult, selectionContext: self.controllerInteraction.selectionState, editingContext: self.controllerInteraction.editingState, updateHiddenMedia: { [weak self] id in
self?.hiddenMediaId.set(.single(id))
}, initialLayout: self.containerLayout?.0, transitionHostView: { [weak self] in
return self?.gridNode.view

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