mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
363 lines
18 KiB
Swift
363 lines
18 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import Postbox
|
|
import TelegramCore
|
|
import TelegramPresentationData
|
|
import LocalizedPeerData
|
|
|
|
public enum ChatMessageForwardInfoType: Equatable {
|
|
case bubble(incoming: Bool)
|
|
case standalone
|
|
}
|
|
|
|
private final class InfoButtonNode: HighlightableButtonNode {
|
|
private let pressed: () -> Void
|
|
let iconNode: ASImageNode
|
|
|
|
private var theme: ChatPresentationThemeData?
|
|
private var type: ChatMessageForwardInfoType?
|
|
|
|
init(pressed: @escaping () -> Void) {
|
|
self.pressed = pressed
|
|
|
|
self.iconNode = ASImageNode()
|
|
self.iconNode.displaysAsynchronously = false
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.iconNode)
|
|
|
|
self.addTarget(self, action: #selector(self.pressedEvent), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
@objc private func pressedEvent() {
|
|
self.pressed()
|
|
}
|
|
|
|
func update(size: CGSize, theme: ChatPresentationThemeData, type: ChatMessageForwardInfoType) {
|
|
if self.theme !== theme || self.type != type {
|
|
self.theme = theme
|
|
self.type = type
|
|
let color: UIColor
|
|
switch type {
|
|
case let .bubble(incoming):
|
|
color = incoming ? theme.theme.chat.message.incoming.accentControlColor : theme.theme.chat.message.outgoing.accentControlColor
|
|
case .standalone:
|
|
let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper)
|
|
color = serviceColor.primaryText
|
|
}
|
|
self.iconNode.image = PresentationResourcesChat.chatPsaInfo(theme.theme, color: color.argb)
|
|
}
|
|
|
|
if let image = self.iconNode.image {
|
|
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
|
|
}
|
|
}
|
|
}
|
|
|
|
public class ChatMessageForwardInfoNode: ASDisplayNode {
|
|
public enum StoryType {
|
|
case regular
|
|
case expired
|
|
case unavailable
|
|
}
|
|
|
|
public struct StoryData: Equatable {
|
|
public var storyType: StoryType
|
|
|
|
public init(storyType: StoryType) {
|
|
self.storyType = storyType
|
|
}
|
|
}
|
|
|
|
private var textNode: TextNode?
|
|
private var credibilityIconNode: ASImageNode?
|
|
private var infoNode: InfoButtonNode?
|
|
private var expiredStoryIconView: UIImageView?
|
|
|
|
public var openPsa: ((String, ASDisplayNode) -> Void)?
|
|
|
|
override public init() {
|
|
super.init()
|
|
}
|
|
|
|
public func hasAction(at point: CGPoint) -> Bool {
|
|
if let infoNode = self.infoNode, infoNode.frame.contains(point) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
public func updatePsaButtonDisplay(isVisible: Bool, animated: Bool) {
|
|
if let infoNode = self.infoNode {
|
|
if isVisible != !infoNode.iconNode.alpha.isZero {
|
|
let transition: ContainedViewLayoutTransition
|
|
if animated {
|
|
transition = .animated(duration: 0.25, curve: .easeInOut)
|
|
} else {
|
|
transition = .immediate
|
|
}
|
|
transition.updateAlpha(node: infoNode.iconNode, alpha: isVisible ? 1.0 : 0.0)
|
|
transition.updateSublayerTransformScale(node: infoNode, scale: isVisible ? 1.0 : 0.1)
|
|
}
|
|
}
|
|
}
|
|
|
|
public static func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ storyData: StoryData?, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) {
|
|
let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode)
|
|
|
|
return { presentationData, strings, type, peer, authorName, psaType, storyData, constrainedSize in
|
|
let fontSize = floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)
|
|
let prefixFont = Font.regular(fontSize)
|
|
let peerFont = Font.medium(fontSize)
|
|
|
|
let peerString: String
|
|
if let peer = peer {
|
|
if let authorName = authorName {
|
|
peerString = "\(EnginePeer(peer).displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)) (\(authorName))"
|
|
} else {
|
|
peerString = EnginePeer(peer).displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)
|
|
}
|
|
} else if let authorName = authorName {
|
|
peerString = authorName
|
|
} else {
|
|
peerString = ""
|
|
}
|
|
|
|
var hasPsaInfo = false
|
|
if let _ = psaType {
|
|
hasPsaInfo = true
|
|
}
|
|
|
|
let titleColor: UIColor
|
|
let completeSourceString: PresentationStrings.FormattedString
|
|
|
|
switch type {
|
|
case let .bubble(incoming):
|
|
if let psaType = psaType {
|
|
titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barPositive : presentationData.theme.theme.chat.message.outgoing.polls.barPositive
|
|
|
|
var customFormat: String?
|
|
let key = "Message.ForwardedPsa.\(psaType)"
|
|
if let string = presentationData.strings.primaryComponent.dict[key] {
|
|
customFormat = string
|
|
} else if let string = presentationData.strings.secondaryComponent?.dict[key] {
|
|
customFormat = string
|
|
}
|
|
|
|
if let customFormat = customFormat {
|
|
if let range = customFormat.range(of: "%@") {
|
|
let leftPart = String(customFormat[customFormat.startIndex ..< range.lowerBound])
|
|
let rightPart = String(customFormat[range.upperBound...])
|
|
|
|
let formattedText = leftPart + peerString + rightPart
|
|
completeSourceString = PresentationStrings.FormattedString(string: formattedText, ranges: [PresentationStrings.FormattedString.Range(index: 0, range: NSRange(location: leftPart.count, length: peerString.count))])
|
|
} else {
|
|
completeSourceString = PresentationStrings.FormattedString(string: customFormat, ranges: [])
|
|
}
|
|
} else {
|
|
completeSourceString = strings.Message_GenericForwardedPsa(peerString)
|
|
}
|
|
} else {
|
|
titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.accentTextColor : presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
|
|
|
if let storyData = storyData {
|
|
switch storyData.storyType {
|
|
case .regular:
|
|
completeSourceString = strings.Message_ForwardedStoryShort(peerString)
|
|
case .expired:
|
|
completeSourceString = strings.Message_ForwardedExpiredStoryShort(peerString)
|
|
case .unavailable:
|
|
completeSourceString = strings.Message_ForwardedUnavailableStoryShort(peerString)
|
|
}
|
|
} else {
|
|
completeSourceString = strings.Message_ForwardedMessageShort(peerString)
|
|
}
|
|
}
|
|
case .standalone:
|
|
let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
|
titleColor = serviceColor.primaryText
|
|
|
|
if let psaType = psaType {
|
|
var customFormat: String?
|
|
let key = "Message.ForwardedPsa.\(psaType)"
|
|
if let string = presentationData.strings.primaryComponent.dict[key] {
|
|
customFormat = string
|
|
} else if let string = presentationData.strings.secondaryComponent?.dict[key] {
|
|
customFormat = string
|
|
}
|
|
|
|
if let customFormat = customFormat {
|
|
if let range = customFormat.range(of: "%@") {
|
|
let leftPart = String(customFormat[customFormat.startIndex ..< range.lowerBound])
|
|
let rightPart = String(customFormat[range.upperBound...])
|
|
|
|
let formattedText = leftPart + peerString + rightPart
|
|
completeSourceString = PresentationStrings.FormattedString(string: formattedText, ranges: [PresentationStrings.FormattedString.Range(index: 0, range: NSRange(location: leftPart.count, length: peerString.count))])
|
|
} else {
|
|
completeSourceString = PresentationStrings.FormattedString(string: customFormat, ranges: [])
|
|
}
|
|
} else {
|
|
completeSourceString = strings.Message_GenericForwardedPsa(peerString)
|
|
}
|
|
} else {
|
|
completeSourceString = strings.Message_ForwardedMessageShort(peerString)
|
|
}
|
|
}
|
|
|
|
var currentCredibilityIconImage: UIImage?
|
|
var highlight = true
|
|
if let peer = peer {
|
|
if let channel = peer as? TelegramChannel, channel.addressName == nil {
|
|
if case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) {
|
|
} else if case .member = channel.participationStatus {
|
|
} else {
|
|
highlight = false
|
|
}
|
|
}
|
|
|
|
if peer.isFake {
|
|
switch type {
|
|
case let .bubble(incoming):
|
|
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, strings: presentationData.strings, type: incoming ? .regular : .outgoing)
|
|
case .standalone:
|
|
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, strings: presentationData.strings, type: .service)
|
|
}
|
|
} else if peer.isScam {
|
|
switch type {
|
|
case let .bubble(incoming):
|
|
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, strings: presentationData.strings, type: incoming ? .regular : .outgoing)
|
|
case .standalone:
|
|
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, strings: presentationData.strings, type: .service)
|
|
}
|
|
} else {
|
|
currentCredibilityIconImage = nil
|
|
}
|
|
} else {
|
|
highlight = false
|
|
}
|
|
|
|
//let completeString: NSString = (completeSourceString.string.replacingOccurrences(of: "\n", with: " \n")) as NSString
|
|
let completeString: NSString = completeSourceString.string as NSString
|
|
let string = NSMutableAttributedString(string: completeString as String, attributes: [NSAttributedString.Key.foregroundColor: titleColor, NSAttributedString.Key.font: prefixFont])
|
|
if highlight, let range = completeSourceString.ranges.first?.range {
|
|
string.addAttributes([NSAttributedString.Key.font: peerFont], range: range)
|
|
}
|
|
|
|
var credibilityIconWidth: CGFloat = 0.0
|
|
if let icon = currentCredibilityIconImage {
|
|
credibilityIconWidth += icon.size.width + 4.0
|
|
}
|
|
|
|
var infoWidth: CGFloat = 0.0
|
|
if hasPsaInfo {
|
|
infoWidth += 32.0
|
|
}
|
|
let leftOffset: CGFloat = 0.0
|
|
infoWidth += leftOffset
|
|
|
|
var cutout: TextNodeCutout?
|
|
if let storyData {
|
|
switch storyData.storyType {
|
|
case .regular, .unavailable:
|
|
break
|
|
case .expired:
|
|
cutout = TextNodeCutout(topLeft: CGSize(width: 16.0, height: 10.0))
|
|
}
|
|
}
|
|
|
|
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - credibilityIconWidth - infoWidth, height: constrainedSize.height), alignment: .natural, cutout: cutout, insets: UIEdgeInsets()))
|
|
|
|
return (CGSize(width: textLayout.size.width + credibilityIconWidth + infoWidth, height: textLayout.size.height), { width in
|
|
let node: ChatMessageForwardInfoNode
|
|
if let maybeNode = maybeNode {
|
|
node = maybeNode
|
|
} else {
|
|
node = ChatMessageForwardInfoNode()
|
|
}
|
|
|
|
let textNode = textApply()
|
|
if node.textNode == nil {
|
|
textNode.isUserInteractionEnabled = false
|
|
node.textNode = textNode
|
|
node.addSubnode(textNode)
|
|
}
|
|
textNode.frame = CGRect(origin: CGPoint(x: leftOffset, y: 0.0), size: textLayout.size)
|
|
|
|
if let storyData, case .expired = storyData.storyType {
|
|
let expiredStoryIconView: UIImageView
|
|
if let current = node.expiredStoryIconView {
|
|
expiredStoryIconView = current
|
|
} else {
|
|
expiredStoryIconView = UIImageView()
|
|
node.expiredStoryIconView = expiredStoryIconView
|
|
node.view.addSubview(expiredStoryIconView)
|
|
}
|
|
|
|
let imageType: ChatExpiredStoryIndicatorType
|
|
switch type {
|
|
case .standalone:
|
|
imageType = .free
|
|
case let .bubble(incoming):
|
|
imageType = incoming ? .incoming : .outgoing
|
|
}
|
|
|
|
expiredStoryIconView.image = PresentationResourcesChat.chatExpiredStoryIndicatorIcon(presentationData.theme.theme, type: imageType)
|
|
if let _ = expiredStoryIconView.image {
|
|
let imageSize = CGSize(width: 18.0, height: 18.0)
|
|
expiredStoryIconView.frame = CGRect(origin: CGPoint(x: -1.0, y: -2.0), size: imageSize)
|
|
}
|
|
} else if let expiredStoryIconView = node.expiredStoryIconView {
|
|
expiredStoryIconView.removeFromSuperview()
|
|
}
|
|
|
|
if let credibilityIconImage = currentCredibilityIconImage {
|
|
let credibilityIconNode: ASImageNode
|
|
if let node = node.credibilityIconNode {
|
|
credibilityIconNode = node
|
|
} else {
|
|
credibilityIconNode = ASImageNode()
|
|
node.credibilityIconNode = credibilityIconNode
|
|
node.addSubnode(credibilityIconNode)
|
|
}
|
|
credibilityIconNode.frame = CGRect(origin: CGPoint(x: textLayout.size.width + 4.0, y: 16.0), size: credibilityIconImage.size)
|
|
credibilityIconNode.image = credibilityIconImage
|
|
} else {
|
|
node.credibilityIconNode?.removeFromSupernode()
|
|
node.credibilityIconNode = nil
|
|
}
|
|
|
|
if hasPsaInfo {
|
|
let infoNode: InfoButtonNode
|
|
if let current = node.infoNode {
|
|
infoNode = current
|
|
} else {
|
|
infoNode = InfoButtonNode(pressed: { [weak node] in
|
|
guard let node = node else {
|
|
return
|
|
}
|
|
if let psaType = psaType, let infoNode = node.infoNode {
|
|
node.openPsa?(psaType, infoNode)
|
|
}
|
|
})
|
|
node.infoNode = infoNode
|
|
node.addSubnode(infoNode)
|
|
}
|
|
let infoButtonSize = CGSize(width: 32.0, height: 32.0)
|
|
let infoButtonFrame = CGRect(origin: CGPoint(x: width - infoButtonSize.width - 2.0, y: 1.0), size: infoButtonSize)
|
|
infoNode.frame = infoButtonFrame
|
|
infoNode.update(size: infoButtonFrame.size, theme: presentationData.theme, type: type)
|
|
} else if let infoNode = node.infoNode {
|
|
node.infoNode = nil
|
|
infoNode.removeFromSupernode()
|
|
}
|
|
|
|
return node
|
|
})
|
|
}
|
|
}
|
|
}
|