mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-03-30 18:49:07 +00:00
286 lines
16 KiB
Swift
286 lines
16 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import ComponentFlow
|
|
import TelegramCore
|
|
import AccountContext
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import TextFormat
|
|
import LocalizedPeerData
|
|
import UrlEscaping
|
|
import TelegramStringFormatting
|
|
import WallpaperBackgroundNode
|
|
import ReactionSelectionNode
|
|
import AnimatedStickerNode
|
|
import TelegramAnimatedStickerNode
|
|
import ChatControllerInteraction
|
|
import ShimmerEffect
|
|
import Markdown
|
|
import ChatMessageBubbleContentNode
|
|
import ChatMessageItemCommon
|
|
import TextNodeWithEntities
|
|
|
|
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? {
|
|
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true)
|
|
}
|
|
|
|
public class ChatMessageDisableCopyProtectionBubbleContentNode: ChatMessageBubbleContentNode {
|
|
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
|
|
private let textNode: TextNodeWithEntities
|
|
private let infoNode: TextNodeWithEntities
|
|
|
|
private var absoluteRect: (CGRect, CGSize)?
|
|
|
|
private var isPlaying: Bool = false
|
|
|
|
override public var disablesClipping: Bool {
|
|
return true
|
|
}
|
|
|
|
override public var visibility: ListViewItemNodeVisibility {
|
|
didSet {
|
|
let wasVisible = oldValue != .none
|
|
let isVisible = self.visibility != .none
|
|
|
|
if wasVisible != isVisible {
|
|
self.visibilityStatus = isVisible
|
|
|
|
switch self.visibility {
|
|
case .none:
|
|
self.textNode.visibilityRect = nil
|
|
case let .visible(_, subRect):
|
|
var subRect = subRect
|
|
subRect.origin.x = 0.0
|
|
subRect.size.width = 10000.0
|
|
self.textNode.visibilityRect = subRect
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var visibilityStatus: Bool? {
|
|
didSet {
|
|
if self.visibilityStatus != oldValue {
|
|
self.updateVisibility()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var fetchDisposable: Disposable?
|
|
private var setupTimestamp: Double?
|
|
|
|
private var cachedTonImage: (UIImage, UIColor)?
|
|
|
|
required public init() {
|
|
self.textNode = TextNodeWithEntities()
|
|
self.textNode.textNode.isUserInteractionEnabled = false
|
|
self.textNode.textNode.displaysAsynchronously = false
|
|
|
|
self.infoNode = TextNodeWithEntities()
|
|
self.infoNode.textNode.isUserInteractionEnabled = false
|
|
self.infoNode.textNode.displaysAsynchronously = false
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.textNode.textNode)
|
|
self.addSubnode(self.infoNode.textNode)
|
|
}
|
|
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.fetchDisposable?.dispose()
|
|
}
|
|
|
|
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
|
|
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
|
let makeInfoLayout = TextNodeWithEntities.asyncLayout(self.infoNode)
|
|
|
|
return { item, layoutConstants, _, _, _, _ in
|
|
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
|
|
|
|
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
|
var bubbleSize = CGSize(width: 246.0, height: 240.0)
|
|
let textConstrainedSize = CGSize(width: bubbleSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude)
|
|
|
|
let incoming: Bool
|
|
if item.message.id.peerId == item.context.account.peerId && item.message.forwardInfo == nil {
|
|
incoming = true
|
|
} else {
|
|
incoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
|
}
|
|
|
|
let textColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
|
|
|
|
var text: String = ""
|
|
var hasActionButtons = false
|
|
if let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .copyProtectionRequest(hasExpired, _, _) = action.action {
|
|
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
|
|
|
let appConfiguration = item.context.currentAppConfiguration.with { $0 }
|
|
let configuration = CopyProtectionConfiguration.with(appConfiguration: appConfiguration)
|
|
let expireDate = item.message.timestamp + configuration.requestExpirePeriod
|
|
|
|
hasActionButtons = incoming && !hasExpired && expireDate > currentTimestamp
|
|
}
|
|
|
|
let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
|
|
if incoming {
|
|
text = "**\(peerName)** would like to enable sharing in this chat, which includes:"
|
|
} else {
|
|
text = "You suggested enabling sharing in this chat, which includes:"
|
|
}
|
|
|
|
var infoText = "forwarding messages\nsaving photos and videos\ncopying messages\ntaking screenshots"
|
|
infoText = " # \(infoText.replacingOccurrences(of: "\n", with: "\n # "))"
|
|
|
|
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
|
|
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
|
|
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: textColor),
|
|
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
|
|
linkAttribute: { url in
|
|
return ("URL", url)
|
|
}
|
|
), textAlignment: .center)
|
|
|
|
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, lineSpacing: 0.2, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
let attributedInfoText = parseMarkdownIntoAttributedString(infoText, attributes: MarkdownAttributes(
|
|
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
|
|
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: textColor),
|
|
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
|
|
linkAttribute: { url in
|
|
return ("URL", url)
|
|
}
|
|
), textAlignment: .left).mutableCopy() as! NSMutableAttributedString
|
|
let ranges = attributedInfoText.string.subranges(of: "#")
|
|
for range in ranges.ranges {
|
|
attributedInfoText.addAttribute(.attachment, value: UIImage(bundleImageName: "Chat/Empty Chat/ListCheckIcon")!, range: NSRange(range, in: attributedInfoText.string))
|
|
}
|
|
|
|
let (infoLayout, infoApply) = makeInfoLayout(TextNodeLayoutArguments(attributedString: attributedInfoText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .left, lineSpacing: 1.0, cutout: nil, insets: UIEdgeInsets(top: 0.0, left: 2.0, bottom: 0.0, right: 2.0)))
|
|
|
|
bubbleSize.height = textLayout.size.height + infoLayout.size.height + 25.0
|
|
|
|
let backgroundSize = CGSize(width: bubbleSize.width, height: bubbleSize.height + 4.0)
|
|
|
|
return (backgroundSize.width, { boundingWidth in
|
|
return (backgroundSize, { [weak self] animation, synchronousLoads, info in
|
|
if let strongSelf = self {
|
|
strongSelf.item = item
|
|
|
|
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - bubbleSize.width) / 2.0), y: 0.0), size: bubbleSize)
|
|
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
|
|
|
|
strongSelf.updateVisibility()
|
|
|
|
let _ = textApply(TextNodeWithEntities.Arguments(
|
|
context: item.context,
|
|
cache: item.controllerInteraction.presentationContext.animationCache,
|
|
renderer: item.controllerInteraction.presentationContext.animationRenderer,
|
|
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
|
|
attemptSynchronous: synchronousLoads
|
|
))
|
|
|
|
let _ = infoApply(TextNodeWithEntities.Arguments(
|
|
context: item.context,
|
|
cache: item.controllerInteraction.presentationContext.animationCache,
|
|
renderer: item.controllerInteraction.presentationContext.animationRenderer,
|
|
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
|
|
attemptSynchronous: synchronousLoads
|
|
))
|
|
|
|
let textFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - textLayout.size.width) / 2.0), y: mediaBackgroundFrame.minY + 14.0), size: textLayout.size)
|
|
strongSelf.textNode.textNode.frame = textFrame
|
|
|
|
let infoFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - infoLayout.size.width) / 2.0), y: mediaBackgroundFrame.minY + 14.0 + textLayout.size.height + 12.0), size: infoLayout.size)
|
|
strongSelf.infoNode.textNode.frame = infoFrame
|
|
|
|
if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
|
|
backgroundContent.clipsToBounds = true
|
|
backgroundContent.cornerRadius = 24.0
|
|
|
|
strongSelf.mediaBackgroundContent = backgroundContent
|
|
strongSelf.insertSubnode(backgroundContent, at: 0)
|
|
}
|
|
|
|
if let backgroundContent = strongSelf.mediaBackgroundContent {
|
|
animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil)
|
|
backgroundContent.clipsToBounds = true
|
|
|
|
if hasActionButtons {
|
|
backgroundContent.cornerRadius = 0.0
|
|
if backgroundContent.view.mask == nil {
|
|
backgroundContent.view.mask = UIImageView(image: generateImage(mediaBackgroundFrame.size, rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: .zero, size: size))
|
|
context.setFillColor(UIColor.white.cgColor)
|
|
|
|
context.addPath(CGPath(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height * 0.5), cornerWidth: 24.0, cornerHeight: 24.0, transform: nil))
|
|
context.addPath(CGPath(roundedRect: CGRect(x: 0, y: size.height * 0.5 - 30.0, width: size.width, height: size.height * 0.5 + 30.0), cornerWidth: 8.0, cornerHeight: 8.0, transform: nil))
|
|
context.fillPath()
|
|
}))
|
|
}
|
|
} else {
|
|
backgroundContent.view.mask = nil
|
|
backgroundContent.cornerRadius = 24.0
|
|
}
|
|
}
|
|
|
|
if let (rect, size) = strongSelf.absoluteRect {
|
|
strongSelf.updateAbsoluteRect(rect, within: size)
|
|
}
|
|
|
|
switch strongSelf.visibility {
|
|
case .none:
|
|
strongSelf.textNode.visibilityRect = nil
|
|
strongSelf.infoNode.visibilityRect = nil
|
|
case let .visible(_, subRect):
|
|
var subRect = subRect
|
|
subRect.origin.x = 0.0
|
|
subRect.size.width = 10000.0
|
|
strongSelf.textNode.visibilityRect = subRect
|
|
strongSelf.infoNode.visibilityRect = subRect
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
|
self.absoluteRect = (rect, containerSize)
|
|
|
|
if let mediaBackgroundContent = self.mediaBackgroundContent {
|
|
var backgroundFrame = mediaBackgroundContent.frame
|
|
backgroundFrame.origin.x += rect.minX
|
|
backgroundFrame.origin.y += rect.minY
|
|
mediaBackgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
|
|
|
|
}
|
|
|
|
override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
|
|
|
|
}
|
|
|
|
override public func unreadMessageRangeUpdated() {
|
|
self.updateVisibility()
|
|
}
|
|
|
|
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
|
return ChatMessageBubbleContentTapAction(content: .none)
|
|
}
|
|
|
|
private func updateVisibility() {
|
|
}
|
|
}
|