Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,420 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramCore
import Postbox
import SwiftSignalKit
import Display
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import LocalizedPeerData
import AlertUI
import PresentationDataUtils
import TextFormat
import Markdown
import TelegramNotices
import ChatPresentationInterfaceState
import TextNodeWithEntities
import AnimationCache
import MultiAnimationRenderer
import AccessoryPanelNode
import AppBundle
func textStringForForwardedMessage(_ message: Message, strings: PresentationStrings) -> (text: String, entities: [MessageTextEntity], isMedia: Bool) {
for media in message.media {
switch media {
case _ as TelegramMediaImage:
return (strings.Message_Photo, [], true)
case let file as TelegramMediaFile:
if file.isVideoSticker || file.isAnimatedSticker {
return (strings.Message_Sticker, [], true)
}
var fileName: String = strings.Message_File
for attribute in file.attributes {
switch attribute {
case .Sticker:
return (strings.Message_Sticker, [], true)
case let .FileName(name):
fileName = name
case let .Audio(isVoice, _, title, performer, _):
if isVoice {
return (strings.Message_Audio, [], true)
} else {
if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty {
return (title + "" + performer, [], true)
} else if let title = title, !title.isEmpty {
return (title, [], true)
} else if let performer = performer, !performer.isEmpty {
return (performer, [], true)
} else {
return (strings.Message_Audio, [], true)
}
}
case .Video:
if file.isAnimated {
return (strings.Message_Animation, [], true)
} else {
return (strings.Message_Video, [], true)
}
default:
break
}
}
return (fileName, [], true)
case _ as TelegramMediaContact:
return (strings.Message_Contact, [], true)
case let game as TelegramMediaGame:
return (game.title, [], true)
case _ as TelegramMediaMap:
return (strings.Message_Location, [], true)
case _ as TelegramMediaAction:
return ("", [], true)
case _ as TelegramMediaPoll:
return (strings.ForwardedPolls(1), [], true)
case let todo as TelegramMediaTodo:
return (todo.text, [], true)
case let dice as TelegramMediaDice:
return (dice.emoji, [], true)
case let invoice as TelegramMediaInvoice:
return (invoice.title, [], true)
default:
break
}
}
return (message.text, message.textEntitiesAttribute?.entities ?? [], false)
}
public final class ForwardAccessoryPanelNode: AccessoryPanelNode {
private let messageDisposable = MetaDisposable()
public let messageIds: [MessageId]
private var messages: [Message] = []
let closeButton: HighlightableButtonNode
let lineNode: ASImageNode
let iconView: UIImageView
let titleNode: ImmediateTextNode
let textNode: ImmediateTextNodeWithEntities
private var originalText: NSAttributedString?
private let actionArea: AccessibilityAreaNode
let context: AccountContext
var theme: PresentationTheme
var strings: PresentationStrings
var fontSize: PresentationFontSize
var nameDisplayOrder: PresentationPersonNameOrder
var forwardOptionsState: ChatInterfaceForwardOptionsState?
private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)?
public init(context: AccountContext, messageIds: [MessageId], theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, nameDisplayOrder: PresentationPersonNameOrder, forwardOptionsState: ChatInterfaceForwardOptionsState?, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) {
self.context = context
self.messageIds = messageIds
self.theme = theme
self.strings = strings
self.fontSize = fontSize
self.nameDisplayOrder = nameDisplayOrder
self.forwardOptionsState = forwardOptionsState
self.closeButton = HighlightableButtonNode()
self.closeButton.accessibilityLabel = strings.VoiceOver_DiscardPreparedContent
self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: [])
self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
self.closeButton.displaysAsynchronously = false
self.lineNode = ASImageNode()
self.lineNode.displayWithoutProcessing = false
self.lineNode.displaysAsynchronously = false
self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme)
self.iconView = UIImageView()
self.iconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/ForwardSettingsIcon")?.withRenderingMode(.alwaysTemplate)
self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.displaysAsynchronously = false
self.textNode = ImmediateTextNodeWithEntities()
self.textNode.maximumNumberOfLines = 1
self.textNode.displaysAsynchronously = false
self.actionArea = AccessibilityAreaNode()
super.init()
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside])
self.addSubnode(self.closeButton)
self.addSubnode(self.lineNode)
self.view.addSubview(self.iconView)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.actionArea)
if let animationCache = animationCache, let animationRenderer = animationRenderer {
self.textNode.arguments = TextNodeWithEntities.Arguments(
context: context,
cache: animationCache,
renderer: animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
attemptSynchronous: false
)
}
self.messageDisposable.set((context.account.postbox.messagesAtIds(messageIds)
|> deliverOnMainQueue).start(next: { [weak self] messages in
if let strongSelf = self {
if messages.isEmpty {
strongSelf.dismiss?()
} else {
strongSelf.messages = messages
if let (size, inset, interfaceState) = strongSelf.validLayout {
strongSelf.updateState(size: size, inset: inset, interfaceState: interfaceState)
}
let _ = (ApplicationSpecificNotice.getChatForwardOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] count in
if let strongSelf = self, count < 3 {
Queue.mainQueue().after(3.0) {
if let snapshotView = strongSelf.textNode.view.snapshotContentTree() {
let text: String
if let (size, _, _) = strongSelf.validLayout, size.width > 320.0 {
text = strongSelf.strings.Conversation_ForwardOptions_TapForOptions
} else {
text = strongSelf.strings.Conversation_ForwardOptions_TapForOptionsShort
}
strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor)
strongSelf.view.addSubview(snapshotView)
if let (size, inset, interfaceState) = strongSelf.validLayout {
strongSelf.updateState(size: size, inset: inset, interfaceState: interfaceState)
}
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager).start()
}
}
})
}
}
}))
}
deinit {
self.messageDisposable.dispose()
}
override public func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
override public func animateIn() {
self.iconView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
}
override public func animateOut() {
self.iconView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
}
override public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.updateThemeAndStrings(theme: theme, strings: strings, forwardOptionsState: self.forwardOptionsState)
}
public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, forwardOptionsState: ChatInterfaceForwardOptionsState?, force: Bool = false) {
if force || self.theme !== theme || self.strings !== strings || self.forwardOptionsState != forwardOptionsState {
self.theme = theme
self.strings = strings
self.forwardOptionsState = forwardOptionsState
self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: [])
self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme)
self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor
let filteredMessages = self.messages
var title = ""
if filteredMessages.count == 1 {
title = self.strings.Conversation_ForwardOptions_ForwardTitleSingle
} else {
title = self.strings.Conversation_ForwardOptions_ForwardTitle(Int32(filteredMessages.count))
}
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor)
if let attributedText = self.textNode.attributedText {
let updatedText = NSMutableAttributedString(attributedString: attributedText)
updatedText.addAttribute(.foregroundColor, value: self.theme.chat.inputPanel.secondaryTextColor, range: NSRange(location: 0, length: updatedText.length))
self.textNode.attributedText = updatedText
self.originalText = updatedText
}
if let (size, inset, interfaceState) = self.validLayout {
self.updateState(size: size, inset: inset, interfaceState: interfaceState)
}
}
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: constrainedSize.width, height: 45.0)
}
override public func updateState(size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState) {
self.validLayout = (size, inset, interfaceState)
let bounds = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 45.0))
let leftInset: CGFloat = 55.0 + inset
let rightInset: CGFloat = 55.0 + inset
let textLineInset: CGFloat = 10.0
let textRightInset: CGFloat = 20.0
let closeButtonSize = CGSize(width: 44.0, height: bounds.height)
let closeButtonFrame = CGRect(origin: CGPoint(x: bounds.width - closeButtonSize.width - inset, y: 2.0), size: closeButtonSize)
self.closeButton.frame = closeButtonFrame
self.closeButton.isHidden = interfaceState.renderedPeer == nil
self.actionArea.frame = CGRect(origin: CGPoint(x: leftInset, y: 2.0), size: CGSize(width: closeButtonFrame.minX - leftInset, height: bounds.height))
self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0))
if let icon = self.iconView.image {
self.iconView.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size)
}
var authors = ""
var uniquePeerIds = Set<PeerId>()
var title = ""
var text = NSMutableAttributedString(string: "")
for message in self.messages {
if let author = message.forwardInfo?.author ?? message.effectiveAuthor, !uniquePeerIds.contains(author.id) {
uniquePeerIds.insert(author.id)
if !authors.isEmpty {
authors.append(", ")
}
if author.id == context.account.peerId {
authors.append(self.strings.DialogList_You)
} else {
authors.append(EnginePeer(author).compactDisplayTitle)
}
}
}
if self.messages.count == 1 {
title = self.strings.Conversation_ForwardOptions_ForwardTitleSingle
let (string, entities, _) = textStringForForwardedMessage(messages[0], strings: strings)
text = NSMutableAttributedString(attributedString: NSAttributedString(string: "\(authors): ", font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.secondaryTextColor))
let additionalText = NSMutableAttributedString(attributedString: NSAttributedString(string: string, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.secondaryTextColor))
for entity in entities {
switch entity.type {
case let .CustomEmoji(_, fileId):
let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
if range.lowerBound >= 0 && range.upperBound <= additionalText.length {
additionalText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: messages[0].associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile), range: range)
}
default:
break
}
}
text.append(additionalText)
} else {
title = self.strings.Conversation_ForwardOptions_ForwardTitle(Int32(messages.count))
text = NSMutableAttributedString(attributedString: NSAttributedString(string: self.strings.Conversation_ForwardFrom(authors).string, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.secondaryTextColor))
}
if interfaceState.interfaceState.forwardOptionsState?.hideNames == true {
text = NSMutableAttributedString(attributedString: NSAttributedString(string: self.strings.Conversation_ForwardOptions_SenderNamesRemoved, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.secondaryTextColor))
}
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor)
self.textNode.attributedText = text
self.originalText = text
self.textNode.visibility = true
let headerString: String
if messages.count == 1 {
headerString = "Forward message"
} else {
headerString = "Forward messages"
}
self.actionArea.accessibilityLabel = "\(headerString). From: \(authors).\n\(text)"
let titleSize = self.titleNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height))
self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 7.0), size: titleSize)
let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height))
self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 25.0), size: textSize)
}
@objc private func closePressed() {
guard let message = self.messages.first, let peer = message.peers[message.id.peerId] else {
return
}
let peerId = peer.id
let peerDisplayTitle = EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder)
let messageCount = Int32(self.messageIds.count)
let messages = self.strings.Conversation_ForwardOptions_Messages(messageCount)
let string: PresentationStrings.FormattedString
if peerId == self.context.account.peerId {
string = self.strings.Conversation_ForwardOptions_TextSaved(messages)
} else if peerId.namespace == Namespaces.Peer.CloudUser {
string = self.strings.Conversation_ForwardOptions_TextPersonal(messages, peerDisplayTitle)
} else {
string = self.strings.Conversation_ForwardOptions_Text(messages, peerDisplayTitle)
}
let font = Font.regular(floor(self.fontSize.baseDisplaySize * 15.0 / 17.0))
let boldFont = Font.semibold(floor(self.fontSize.baseDisplaySize * 15.0 / 17.0))
let body = MarkdownAttributeSet(font: font, textColor: self.theme.actionSheet.secondaryTextColor)
let bold = MarkdownAttributeSet(font: boldFont, textColor: self.theme.actionSheet.secondaryTextColor)
let title = NSAttributedString(string: self.strings.Conversation_ForwardOptions_Title(messageCount), font: Font.semibold(floor(self.fontSize.baseDisplaySize)), textColor: self.theme.actionSheet.primaryTextColor, paragraphAlignment: .center)
let text = addAttributesToStringWithRanges(string._tuple, body: body, argumentAttributes: [0: bold, 1: bold], textAlignment: .center)
let alertController = richTextAlertController(context: self.context, title: title, text: text, actions: [TextAlertAction(type: .genericAction, title: self.strings.Conversation_ForwardOptions_ShowOptions, action: { [weak self] in
if let strongSelf = self {
strongSelf.interfaceInteraction?.presentForwardOptions(strongSelf.view)
Queue.mainQueue().after(0.5) {
strongSelf.updateThemeAndStrings(theme: strongSelf.theme, strings: strongSelf.strings, forwardOptionsState: strongSelf.forwardOptionsState, force: true)
}
let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 3).start()
}
}), TextAlertAction(type: .destructiveAction, title: self.strings.Conversation_ForwardOptions_CancelForwarding, action: { [weak self] in
self?.dismiss?()
})], actionLayout: .vertical)
self.interfaceInteraction?.presentController(alertController, nil)
}
private var previousTapTimestamp: Double?
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let timestamp = CFAbsoluteTimeGetCurrent()
if let previousTapTimestamp = self.previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp {
return
}
self.previousTapTimestamp = CFAbsoluteTimeGetCurrent()
self.interfaceInteraction?.presentForwardOptions(self.view)
Queue.mainQueue().after(1.5) {
self.updateThemeAndStrings(theme: self.theme, strings: self.strings, forwardOptionsState: self.forwardOptionsState, force: true)
}
let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: self.context.sharedContext.accountManager, count: 3).start()
}
}
}