mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
Refactoring
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatMessageMapBubbleContentNode",
|
||||
module_name = "ChatMessageMapBubbleContentNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/LiveLocationTimerNode",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/MediaResources",
|
||||
"//submodules/LocationResources",
|
||||
"//submodules/LiveLocationPositionNode",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TelegramStringFormatting
|
||||
|
||||
private let textFont: UIFont = Font.regular(14.0)
|
||||
|
||||
private class ChatMessageLiveLocationTextNodeParams: NSObject {
|
||||
let color: UIColor
|
||||
let string: String
|
||||
|
||||
init(color: UIColor, string: String) {
|
||||
self.color = color
|
||||
self.string = string
|
||||
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
private final class RadialTimeoutNodeTimer: NSObject {
|
||||
let action: () -> Void
|
||||
init(_ action: @escaping () -> Void) {
|
||||
self.action = action
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc func event() {
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatMessageLiveLocationTextNode: ASDisplayNode {
|
||||
private var timeoutAndColors: (UIColor, Double, PresentationStrings, PresentationDateTimeFormat)?
|
||||
private var updateTimer: Timer?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.isOpaque = false
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.updateTimer?.invalidate()
|
||||
}
|
||||
|
||||
public func update(color: UIColor, timestamp: Double, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) {
|
||||
if self.timeoutAndColors?.1 != timestamp {
|
||||
self.updateTimer?.invalidate()
|
||||
self.timeoutAndColors = (color, timestamp, strings, dateTimeFormat)
|
||||
|
||||
let updateTimer = Timer(timeInterval: 30.0, target: RadialTimeoutNodeTimer({ [weak self] in
|
||||
self?.setNeedsDisplay()
|
||||
}), selector: #selector(RadialTimeoutNodeTimer.event), userInfo: nil, repeats: true)
|
||||
self.updateTimer = updateTimer
|
||||
RunLoop.main.add(updateTimer, forMode: .common)
|
||||
}
|
||||
}
|
||||
|
||||
public override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
if let (color, updateTimestamp, strings, dateTimeFormat) = self.timeoutAndColors {
|
||||
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||
|
||||
let string = stringForRelativeLiveLocationTimestamp(strings: strings, relativeTimestamp: Int32(updateTimestamp), relativeTo: Int32(timestamp), dateTimeFormat: dateTimeFormat)
|
||||
|
||||
return ChatMessageLiveLocationTextNodeParams(color: color, string: string)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
|
||||
if !isRasterizing {
|
||||
context.setBlendMode(.copy)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fill(bounds)
|
||||
}
|
||||
|
||||
if let parameters = parameters as? ChatMessageLiveLocationTextNodeParams {
|
||||
let attributes: [NSAttributedString.Key: Any] = [.font: textFont, .foregroundColor: parameters.color]
|
||||
let nsString = parameters.string as NSString
|
||||
nsString.draw(at: CGPoint(x: 0.0, y: 0.0), withAttributes: attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import LiveLocationTimerNode
|
||||
import PhotoResources
|
||||
import MediaResources
|
||||
import LocationResources
|
||||
import LiveLocationPositionNode
|
||||
import ChatMessageDateAndStatusNode
|
||||
import ChatMessageBubbleContentNode
|
||||
import ChatMessageItemCommon
|
||||
|
||||
private let titleFont = Font.medium(14.0)
|
||||
private let liveTitleFont = Font.medium(16.0)
|
||||
private let textFont = Font.regular(14.0)
|
||||
|
||||
public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let imageNode: TransformImageNode
|
||||
private let pinNode: ChatMessageLiveLocationPositionNode
|
||||
private let dateAndStatusNode: ChatMessageDateAndStatusNode
|
||||
private let titleNode: TextNode
|
||||
private let textNode: TextNode
|
||||
private var liveTimerNode: ChatMessageLiveLocationTimerNode?
|
||||
private var liveTextNode: ChatMessageLiveLocationTextNode?
|
||||
|
||||
private var media: TelegramMediaMap?
|
||||
|
||||
private var timeoutTimer: (SwiftSignalKit.Timer, Int32)?
|
||||
|
||||
required public init() {
|
||||
self.imageNode = TransformImageNode()
|
||||
self.imageNode.contentAnimations = [.subsequentUpdates]
|
||||
self.pinNode = ChatMessageLiveLocationPositionNode()
|
||||
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
|
||||
self.titleNode = TextNode()
|
||||
self.textNode = TextNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.pinNode)
|
||||
}
|
||||
|
||||
override public func accessibilityActivate() -> Bool {
|
||||
if let item = self.item {
|
||||
let _ = item.controllerInteraction.openMessage(item.message, .default)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.timeoutTimer?.0.invalidate()
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:)))
|
||||
self.view.addGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
|
||||
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
|
||||
let makeImageLayout = self.imageNode.asyncLayout()
|
||||
let makePinLayout = self.pinNode.asyncLayout()
|
||||
let statusLayout = self.dateAndStatusNode.asyncLayout()
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
|
||||
let previousMedia = self.media
|
||||
|
||||
return { item, layoutConstants, preparePosition, _, constrainedSize, _ in
|
||||
var selectedMedia: TelegramMediaMap?
|
||||
var activeLiveBroadcastingTimeout: Int32?
|
||||
for media in item.message.media {
|
||||
if let telegramMap = media as? TelegramMediaMap {
|
||||
selectedMedia = telegramMap
|
||||
if let liveBroadcastingTimeout = telegramMap.liveBroadcastingTimeout {
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
if item.message.timestamp != scheduleWhenOnlineTimestamp && item.message.timestamp + liveBroadcastingTimeout > timestamp {
|
||||
activeLiveBroadcastingTimeout = liveBroadcastingTimeout
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
||||
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
|
||||
incoming = false
|
||||
}
|
||||
|
||||
let bubbleInsets: UIEdgeInsets
|
||||
if case .color = item.presentationData.theme.wallpaper {
|
||||
bubbleInsets = UIEdgeInsets()
|
||||
} else {
|
||||
bubbleInsets = layoutConstants.image.bubbleInsets
|
||||
}
|
||||
|
||||
var titleString: NSAttributedString?
|
||||
var textString: NSAttributedString?
|
||||
|
||||
let imageSize: CGSize
|
||||
if let selectedMedia = selectedMedia {
|
||||
if activeLiveBroadcastingTimeout != nil || selectedMedia.venue != nil {
|
||||
let fitWidth: CGFloat = min(constrainedSize.width, layoutConstants.image.maxDimensions.width)
|
||||
|
||||
imageSize = CGSize(width: fitWidth, height: floor(fitWidth * 0.5))
|
||||
|
||||
if let venue = selectedMedia.venue {
|
||||
titleString = NSAttributedString(string: venue.title, font: titleFont, textColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor)
|
||||
if let address = venue.address, !address.isEmpty {
|
||||
textString = NSAttributedString(string: address, font: textFont, textColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor)
|
||||
}
|
||||
} else {
|
||||
textString = NSAttributedString(string: " ", font: textFont, textColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor)
|
||||
}
|
||||
} else {
|
||||
let fitWidth: CGFloat = min(constrainedSize.width, layoutConstants.image.maxDimensions.width)
|
||||
|
||||
imageSize = CGSize(width: fitWidth, height: floor(fitWidth * 0.5))
|
||||
}
|
||||
|
||||
if selectedMedia.liveBroadcastingTimeout != nil {
|
||||
titleString = NSAttributedString(string: item.presentationData.strings.Message_LiveLocation, font: liveTitleFont, textColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor)
|
||||
}
|
||||
} else {
|
||||
imageSize = CGSize(width: 75.0, height: 75.0)
|
||||
}
|
||||
|
||||
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
if let selectedMedia = selectedMedia, previousMedia == nil || !previousMedia!.isEqual(to: selectedMedia) {
|
||||
var updated = true
|
||||
if let previousMedia = previousMedia {
|
||||
if previousMedia.latitude.isEqual(to: selectedMedia.latitude) && previousMedia.longitude.isEqual(to: selectedMedia.longitude) {
|
||||
updated = false
|
||||
}
|
||||
}
|
||||
if updated {
|
||||
updateImageSignal = chatMapSnapshotImage(engine: item.context.engine, resource: MapSnapshotMediaResource(latitude: selectedMedia.latitude, longitude: selectedMedia.longitude, width: Int32(imageSize.width), height: Int32(imageSize.height)))
|
||||
}
|
||||
}
|
||||
|
||||
let maximumWidth: CGFloat
|
||||
if activeLiveBroadcastingTimeout != nil || selectedMedia?.venue != nil {
|
||||
maximumWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right
|
||||
} else {
|
||||
maximumWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right
|
||||
}
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 5.0, hidesBackground: (activeLiveBroadcastingTimeout == nil && selectedMedia?.venue == nil) ? .emptyWallpaper : .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
var mode: ChatMessageLiveLocationPositionNode.Mode = .location(selectedMedia)
|
||||
if let selectedMedia = selectedMedia, let peer = item.message.author {
|
||||
if selectedMedia.liveBroadcastingTimeout != nil {
|
||||
mode = .liveLocation(peer: EnginePeer(peer), active: activeLiveBroadcastingTimeout != nil, latitude: selectedMedia.latitude, longitude: selectedMedia.longitude, heading: selectedMedia.heading)
|
||||
}
|
||||
}
|
||||
let (pinSize, pinApply) = makePinLayout(item.context, item.presentationData.theme.theme, mode)
|
||||
|
||||
return (contentProperties, nil, maximumWidth, { constrainedSize, position in
|
||||
let imageCorners: ImageCorners
|
||||
let maxTextWidth: CGFloat
|
||||
|
||||
if activeLiveBroadcastingTimeout != nil || selectedMedia?.venue != nil {
|
||||
var relativePosition = position
|
||||
if case let .linear(top, _) = position {
|
||||
relativePosition = .linear(top: top, bottom: .Neighbour(false, .freeform, .default))
|
||||
}
|
||||
imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: relativePosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius, layoutConstants: layoutConstants, chatPresentationData: item.presentationData)
|
||||
|
||||
maxTextWidth = constrainedSize.width - bubbleInsets.left + bubbleInsets.right - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 40.0
|
||||
} else {
|
||||
maxTextWidth = constrainedSize.width - imageSize.width - bubbleInsets.left + bubbleInsets.right - layoutConstants.text.bubbleInsets.right
|
||||
|
||||
imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius, layoutConstants: layoutConstants, chatPresentationData: item.presentationData)
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, maxTextWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: max(1.0, maxTextWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var edited = false
|
||||
if item.attributes.updatingMedia != nil {
|
||||
edited = true
|
||||
}
|
||||
var viewCount: Int?
|
||||
var dateReplies = 0
|
||||
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: item.associatedData.accountPeer, message: item.message)
|
||||
if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) {
|
||||
dateReactionsAndPeers = ([], [])
|
||||
}
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? EditedMessageAttribute {
|
||||
edited = !attribute.isHidden
|
||||
} else if let attribute = attribute as? ViewCountMessageAttribute {
|
||||
viewCount = attribute.count
|
||||
} else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation {
|
||||
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info {
|
||||
dateReplies = Int(attribute.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let selectedMedia = selectedMedia {
|
||||
if selectedMedia.liveBroadcastingTimeout != nil {
|
||||
edited = false
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData)
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
switch position {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if selectedMedia?.venue != nil || activeLiveBroadcastingTimeout != nil {
|
||||
if incoming {
|
||||
statusType = .BubbleIncoming
|
||||
} else {
|
||||
if item.message.flags.contains(.Failed) {
|
||||
statusType = .BubbleOutgoing(.Failed)
|
||||
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
|
||||
statusType = .BubbleOutgoing(.Sending)
|
||||
} else {
|
||||
statusType = .BubbleOutgoing(.Sent(read: item.read))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if incoming {
|
||||
statusType = .ImageIncoming
|
||||
} else {
|
||||
if item.message.flags.contains(.Failed) {
|
||||
statusType = .ImageOutgoing(.Failed)
|
||||
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
|
||||
statusType = .ImageOutgoing(.Sending)
|
||||
} else {
|
||||
statusType = .ImageOutgoing(.Sent(read: item.read))
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
statusType = nil
|
||||
}
|
||||
|
||||
var statusSize = CGSize()
|
||||
var statusApply: ((ListViewItemUpdateAnimation) -> Void)?
|
||||
|
||||
if let statusType = statusType {
|
||||
var isReplyThread = false
|
||||
if case .replyThread = item.chatLocation {
|
||||
isReplyThread = true
|
||||
}
|
||||
|
||||
let statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited,
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||
constrainedSize: CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude),
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: dateReactionsAndPeers.reactions,
|
||||
reactionPeers: dateReactionsAndPeers.peers,
|
||||
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
|
||||
replyCount: dateReplies,
|
||||
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
|
||||
hasAutoremove: item.message.isSelfExpiring,
|
||||
canViewReactionList: canViewMessageReactionList(message: item.message),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
|
||||
))
|
||||
|
||||
let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0)
|
||||
|
||||
statusSize = dateAndStatusSize
|
||||
statusApply = dateAndStatusApply
|
||||
}
|
||||
|
||||
let contentWidth: CGFloat
|
||||
if let selectedMedia = selectedMedia, selectedMedia.liveBroadcastingTimeout != nil || selectedMedia.venue != nil {
|
||||
contentWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right
|
||||
} else {
|
||||
contentWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right
|
||||
}
|
||||
|
||||
return (contentWidth, { boundingWidth in
|
||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor)
|
||||
|
||||
let imageLayoutSize = CGSize(width: imageSize.width + bubbleInsets.left + bubbleInsets.right, height: imageSize.height + bubbleInsets.top + bubbleInsets.bottom)
|
||||
|
||||
let layoutSize: CGSize
|
||||
let statusFrame: CGRect
|
||||
|
||||
let baseImageFrame = CGRect(origin: CGPoint(x: -arguments.insets.left, y: -arguments.insets.top), size: arguments.drawingSize)
|
||||
|
||||
let imageFrame: CGRect
|
||||
|
||||
if activeLiveBroadcastingTimeout != nil || selectedMedia?.venue != nil {
|
||||
layoutSize = CGSize(width: imageLayoutSize.width + bubbleInsets.left, height: imageLayoutSize.height + 1.0 + titleLayout.size.height + 1.0 + textLayout.size.height + 10.0)
|
||||
|
||||
imageFrame = baseImageFrame.offsetBy(dx: bubbleInsets.left, dy: bubbleInsets.top)
|
||||
|
||||
statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - layoutConstants.text.bubbleInsets.right, y: layoutSize.height - statusSize.height - 5.0 - 4.0), size: statusSize)
|
||||
} else {
|
||||
layoutSize = CGSize(width: max(imageLayoutSize.width, statusSize.width + bubbleInsets.left + bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right), height: imageLayoutSize.height)
|
||||
statusFrame = CGRect(origin: CGPoint(x: layoutSize.width - bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize)
|
||||
imageFrame = baseImageFrame.offsetBy(dx: bubbleInsets.left, dy: bubbleInsets.top)
|
||||
}
|
||||
|
||||
let imageApply = makeImageLayout(arguments)
|
||||
|
||||
return (layoutSize, { [weak self] animation, _, _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.media = selectedMedia
|
||||
|
||||
strongSelf.imageNode.frame = imageFrame
|
||||
|
||||
var transition: ContainedViewLayoutTransition = .immediate
|
||||
if case let .System(duration, _) = animation {
|
||||
transition = .animated(duration: duration, curve: .spring)
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
let _ = textApply()
|
||||
|
||||
transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: activeLiveBroadcastingTimeout != nil ? 0.0 : 1.0)
|
||||
|
||||
if let selectedMedia = selectedMedia, selectedMedia.liveBroadcastingTimeout != nil {
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + 7.0, y: imageFrame.maxY + 6.0), size: titleLayout.size)
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + 7.0, y: imageFrame.maxY + 6.0 + titleLayout.size.height), size: textLayout.size)
|
||||
transition.updateAlpha(node: strongSelf.titleNode, alpha: activeLiveBroadcastingTimeout != nil ? 1.0 : 0.0)
|
||||
transition.updateAlpha(node: strongSelf.textNode, alpha: activeLiveBroadcastingTimeout != nil ? 1.0 : 0.0)
|
||||
} else if selectedMedia?.venue != nil {
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + 7.0, y: imageFrame.maxY + 6.0), size: titleLayout.size)
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + 7.0, y: imageFrame.maxY + 6.0 + titleLayout.size.height), size: textLayout.size)
|
||||
transition.updateAlpha(node: strongSelf.titleNode, alpha: 1.0)
|
||||
transition.updateAlpha(node: strongSelf.textNode, alpha: 1.0)
|
||||
} else {
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX + 7.0, y: imageFrame.minY + 1.0), size: titleLayout.size)
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX + 7.0, y: imageFrame.minY + 19.0), size: textLayout.size)
|
||||
}
|
||||
|
||||
if let statusApply = statusApply {
|
||||
if strongSelf.dateAndStatusNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.dateAndStatusNode)
|
||||
}
|
||||
statusApply(animation)
|
||||
strongSelf.dateAndStatusNode.frame = statusFrame.offsetBy(dx: imageFrame.minX, dy: imageFrame.minY)
|
||||
} else if strongSelf.dateAndStatusNode.supernode != nil {
|
||||
strongSelf.dateAndStatusNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let _ = titleString {
|
||||
if strongSelf.titleNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.titleNode)
|
||||
}
|
||||
if strongSelf.textNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.textNode)
|
||||
}
|
||||
} else {
|
||||
if strongSelf.titleNode.supernode != nil {
|
||||
strongSelf.titleNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.textNode.supernode != nil {
|
||||
strongSelf.textNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
if let updateImageSignal = updateImageSignal {
|
||||
strongSelf.imageNode.setSignal(updateImageSignal)
|
||||
}
|
||||
|
||||
if let activeLiveBroadcastingTimeout = activeLiveBroadcastingTimeout {
|
||||
if strongSelf.liveTimerNode == nil {
|
||||
let liveTimerNode = ChatMessageLiveLocationTimerNode()
|
||||
strongSelf.liveTimerNode = liveTimerNode
|
||||
strongSelf.addSubnode(liveTimerNode)
|
||||
}
|
||||
let timerSize = CGSize(width: 28.0, height: 28.0)
|
||||
strongSelf.liveTimerNode?.frame = CGRect(origin: CGPoint(x: floor(imageFrame.maxX - 10.0 - timerSize.width), y: floor(imageFrame.maxY + 11.0)), size: timerSize)
|
||||
|
||||
let timerForegroundColor: UIColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.accentControlColor : item.presentationData.theme.theme.chat.message.outgoing.accentControlColor
|
||||
let timerTextColor: UIColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
|
||||
strongSelf.liveTimerNode?.update(backgroundColor: timerForegroundColor.withAlphaComponent(0.4), foregroundColor: timerForegroundColor, textColor: timerTextColor, beginTimestamp: Double(item.message.timestamp), timeout: Double(activeLiveBroadcastingTimeout), strings: item.presentationData.strings)
|
||||
|
||||
if strongSelf.liveTextNode == nil {
|
||||
let liveTextNode = ChatMessageLiveLocationTextNode()
|
||||
strongSelf.liveTextNode = liveTextNode
|
||||
strongSelf.addSubnode(liveTextNode)
|
||||
}
|
||||
strongSelf.liveTextNode?.frame = CGRect(origin: CGPoint(x: imageFrame.minX + 7.0, y: imageFrame.maxY + 6.0 + titleLayout.size.height), size: CGSize(width: imageFrame.size.width - 14.0 - 40.0, height: 18.0))
|
||||
|
||||
var updateTimestamp = item.message.timestamp
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? EditedMessageAttribute {
|
||||
updateTimestamp = attribute.date
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.liveTextNode?.update(color: timerTextColor, timestamp: Double(updateTimestamp), strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
|
||||
|
||||
let timeoutDeadline = item.message.timestamp + activeLiveBroadcastingTimeout
|
||||
if strongSelf.timeoutTimer?.1 != timeoutDeadline {
|
||||
strongSelf.timeoutTimer?.0.invalidate()
|
||||
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
|
||||
let timer = SwiftSignalKit.Timer(timeout: Double(max(0, timeoutDeadline - currentTimestamp)), repeat: false, completion: {
|
||||
if let strongSelf = self {
|
||||
strongSelf.timeoutTimer?.0.invalidate()
|
||||
strongSelf.timeoutTimer = nil
|
||||
item.controllerInteraction.requestMessageUpdate(item.message.id, false)
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
strongSelf.timeoutTimer = (timer, timeoutDeadline)
|
||||
timer.start()
|
||||
}
|
||||
} else {
|
||||
if let liveTimerNode = strongSelf.liveTimerNode {
|
||||
strongSelf.liveTimerNode = nil
|
||||
transition.updateAlpha(node: liveTimerNode, alpha: 0.0, completion: { [weak liveTimerNode] _ in
|
||||
liveTimerNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
if let liveTextNode = strongSelf.liveTextNode {
|
||||
strongSelf.liveTextNode = nil
|
||||
transition.updateAlpha(node: liveTextNode, alpha: 0.0, completion: { [weak liveTextNode] _ in
|
||||
liveTextNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
if let (timer, _) = strongSelf.timeoutTimer {
|
||||
strongSelf.timeoutTimer = nil
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
imageApply()
|
||||
|
||||
strongSelf.pinNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + floor((imageFrame.size.width - pinSize.width) / 2.0), y: imageFrame.minY + floor(imageFrame.size.height * 0.5 - 10.0 - pinSize.height / 2.0)), size: pinSize)
|
||||
|
||||
pinApply()
|
||||
|
||||
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
|
||||
strongSelf.dateAndStatusNode.pressed = {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode)
|
||||
}
|
||||
} else {
|
||||
strongSelf.dateAndStatusNode.pressed = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
if self.item?.message.id == messageId, let currentMedia = self.media, currentMedia.isEqual(to: media) {
|
||||
let imageNode = self.imageNode
|
||||
return (self.imageNode, self.imageNode.bounds, { [weak imageNode] in
|
||||
return (imageNode?.view.snapshotContentTree(unhide: true), nil)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {
|
||||
var mediaHidden = false
|
||||
if let currentMedia = self.media, let media = media {
|
||||
for item in media {
|
||||
if item.isEqual(to: currentMedia) {
|
||||
mediaHidden = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.imageNode.isHidden = mediaHidden
|
||||
return mediaHidden
|
||||
}
|
||||
|
||||
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
||||
return .none
|
||||
}
|
||||
|
||||
@objc private func imageTap(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
if let item = self.item {
|
||||
let _ = item.controllerInteraction.openMessage(item.message, .default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
|
||||
if !self.dateAndStatusNode.isHidden {
|
||||
return self.dateAndStatusNode.reactionView(value: value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user