mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
2290 lines
140 KiB
Swift
2290 lines
140 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Postbox
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import TelegramCore
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import TextFormat
|
|
import AccountContext
|
|
import UrlEscaping
|
|
import PhotoResources
|
|
import WebsiteType
|
|
import ChatMessageInteractiveMediaBadge
|
|
import GalleryData
|
|
import TextNodeWithEntities
|
|
import AnimationCache
|
|
import MultiAnimationRenderer
|
|
import ChatControllerInteraction
|
|
import ShimmerEffect
|
|
import ChatMessageDateAndStatusNode
|
|
import ChatHistoryEntry
|
|
import ChatMessageItemCommon
|
|
import ChatMessageBubbleContentNode
|
|
import ChatMessageInteractiveInstantVideoNode
|
|
import ChatMessageInteractiveFileNode
|
|
import ChatMessageInteractiveMediaNode
|
|
import WallpaperPreviewMedia
|
|
import ChatMessageAttachedContentButtonNode
|
|
import MessageInlineBlockBackgroundView
|
|
|
|
public enum ChatMessageAttachedContentActionIcon {
|
|
case instant
|
|
case link
|
|
}
|
|
|
|
public struct ChatMessageAttachedContentNodeMediaFlags: OptionSet {
|
|
public var rawValue: Int32
|
|
|
|
public init(rawValue: Int32) {
|
|
self.rawValue = rawValue
|
|
}
|
|
|
|
public init() {
|
|
self.rawValue = 0
|
|
}
|
|
|
|
public static let preferMediaInline = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 0)
|
|
public static let preferMediaBeforeText = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 1)
|
|
public static let preferMediaAspectFilled = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 2)
|
|
public static let titleBeforeMedia = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 3)
|
|
}
|
|
|
|
public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|
private var backgroundView: MessageInlineBlockBackgroundView?
|
|
|
|
private let transformContainer: ASDisplayNode
|
|
private var title: TextNodeWithEntities?
|
|
private var subtitle: TextNodeWithEntities?
|
|
private var text: TextNodeWithEntities?
|
|
private var inlineMedia: TransformImageNode?
|
|
private var contentMedia: ChatMessageInteractiveMediaNode?
|
|
private var contentInstantVideo: ChatMessageInteractiveInstantVideoNode?
|
|
private var contentFile: ChatMessageInteractiveFileNode?
|
|
private var actionButton: ChatMessageAttachedContentButtonNode?
|
|
private var actionButtonSeparator: SimpleLayer?
|
|
public var statusNode: ChatMessageDateAndStatusNode?
|
|
|
|
private var inlineMediaValue: Media?
|
|
|
|
//private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge?
|
|
private var linkHighlightingNode: LinkHighlightingNode?
|
|
|
|
private var context: AccountContext?
|
|
private var message: Message?
|
|
private var media: Media?
|
|
private var theme: ChatPresentationThemeData?
|
|
|
|
private var isHighlighted: Bool = false
|
|
private var highlightTimer: Foundation.Timer?
|
|
|
|
public var openMedia: ((InteractiveMediaNodeActivateContent) -> Void)?
|
|
public var activateAction: (() -> Void)?
|
|
public var requestUpdateLayout: (() -> Void)?
|
|
|
|
private var currentProgressDisposable: Disposable?
|
|
|
|
public var defaultContentAction: () -> ChatMessageBubbleContentTapAction = { return ChatMessageBubbleContentTapAction(content: .none) }
|
|
|
|
public var visibility: ListViewItemNodeVisibility = .none {
|
|
didSet {
|
|
if oldValue != self.visibility {
|
|
self.contentMedia?.visibility = self.visibility != .none
|
|
self.contentInstantVideo?.visibility = self.visibility != .none
|
|
|
|
switch self.visibility {
|
|
case .none:
|
|
self.text?.visibilityRect = nil
|
|
case let .visible(_, subRect):
|
|
var subRect = subRect
|
|
subRect.origin.x = 0.0
|
|
subRect.size.width = 10000.0
|
|
self.text?.visibilityRect = subRect
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override public init() {
|
|
self.transformContainer = ASDisplayNode()
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.transformContainer)
|
|
}
|
|
|
|
deinit {
|
|
self.highlightTimer?.invalidate()
|
|
}
|
|
|
|
@objc private func pressed() {
|
|
self.activateAction?()
|
|
}
|
|
|
|
public typealias AsyncLayout = (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize, _ animationCache: AnimationCache, _ animationRenderer: MultiAnimationRenderer) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)))
|
|
|
|
public func makeProgress() -> Promise<Bool> {
|
|
let progress = Promise<Bool>()
|
|
self.currentProgressDisposable?.dispose()
|
|
self.currentProgressDisposable = (progress.get()
|
|
|> distinctUntilChanged
|
|
|> deliverOnMainQueue).start(next: { [weak self] hasProgress in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.backgroundView?.displayProgress = hasProgress
|
|
})
|
|
return progress
|
|
}
|
|
|
|
public func asyncLayout() -> AsyncLayout {
|
|
let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.title)
|
|
let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitle)
|
|
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.text)
|
|
let makeContentMedia = ChatMessageInteractiveMediaNode.asyncLayout(self.contentMedia)
|
|
let makeContentFile = ChatMessageInteractiveFileNode.asyncLayout(self.contentFile)
|
|
let makeActionButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.actionButton)
|
|
let makeStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode)
|
|
|
|
return { [weak self] presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, animationCache, animationRenderer in
|
|
let isPreview = presentationData.isPreview
|
|
let fontSize: CGFloat
|
|
if message.adAttribute != nil {
|
|
fontSize = floor(presentationData.fontSize.baseDisplaySize)
|
|
} else {
|
|
fontSize = floor(presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
|
|
}
|
|
|
|
let titleFont = Font.semibold(fontSize)
|
|
let textFont = Font.regular(fontSize)
|
|
let textBoldFont = Font.semibold(fontSize)
|
|
let textItalicFont = Font.italic(fontSize)
|
|
let textBoldItalicFont = Font.semiboldItalic(fontSize)
|
|
let textFixedFont = Font.regular(fontSize)
|
|
let textBlockQuoteFont = Font.regular(fontSize)
|
|
|
|
var incoming = message.effectivelyIncoming(context.account.peerId)
|
|
if let subject = associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
|
|
incoming = false
|
|
}
|
|
|
|
var isReplyThread = false
|
|
if case .replyThread = chatLocation {
|
|
isReplyThread = true
|
|
}
|
|
|
|
let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing
|
|
let author = message.author
|
|
let nameColors = author?.nameColor.flatMap { context.peerNameColors.get($0, dark: presentationData.theme.theme.overallDarkAppearance) }
|
|
|
|
let mainColor: UIColor
|
|
var secondaryColor: UIColor?
|
|
var tertiaryColor: UIColor?
|
|
if !incoming {
|
|
mainColor = messageTheme.accentTextColor
|
|
if let _ = nameColors?.secondary {
|
|
secondaryColor = .clear
|
|
}
|
|
if let _ = nameColors?.tertiary {
|
|
tertiaryColor = .clear
|
|
}
|
|
} else {
|
|
var authorNameColor: UIColor?
|
|
authorNameColor = nameColors?.main
|
|
secondaryColor = nameColors?.secondary
|
|
tertiaryColor = nameColors?.tertiary
|
|
|
|
if let authorNameColor {
|
|
mainColor = authorNameColor
|
|
} else {
|
|
mainColor = messageTheme.accentTextColor
|
|
}
|
|
}
|
|
|
|
let textTopSpacing: CGFloat
|
|
let textBottomSpacing: CGFloat
|
|
|
|
if displayLine {
|
|
textTopSpacing = 3.0
|
|
textBottomSpacing = 3.0
|
|
} else {
|
|
textTopSpacing = -2.0
|
|
textBottomSpacing = 0.0
|
|
}
|
|
|
|
let textLineSpacing: CGFloat = 0.09
|
|
let titleTextSpacing: CGFloat = 0.0
|
|
let textContentMediaSpacing: CGFloat = 6.0
|
|
let contentMediaTopSpacing: CGFloat = 6.0
|
|
let contentMediaBottomSpacing: CGFloat = 6.0
|
|
let contentMediaButtonSpacing: CGFloat = 7.0
|
|
let textButtonSpacing: CGFloat = 7.0
|
|
let buttonBottomSpacing: CGFloat = 0.0
|
|
let statusBackgroundSpacing: CGFloat = 9.0
|
|
let inlineMediaEdgeInset: CGFloat = 6.0
|
|
|
|
var insets = UIEdgeInsets()
|
|
insets.left = layoutConstants.text.bubbleInsets.left
|
|
insets.right = layoutConstants.text.bubbleInsets.right
|
|
|
|
if case let .linear(top, _) = preparePosition {
|
|
switch top {
|
|
case .None:
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if displayLine {
|
|
insets.left += 9.0
|
|
insets.right += 6.0
|
|
}
|
|
|
|
var contentMediaValue: Media?
|
|
var contentFileValue: TelegramMediaFile?
|
|
|
|
var contentMediaAutomaticPlayback: Bool = false
|
|
var contentMediaAutomaticDownload: InteractiveMediaNodeAutodownloadMode = .none
|
|
|
|
var mediaAndFlags = mediaAndFlags
|
|
if let mediaAndFlagsValue = mediaAndFlags {
|
|
if mediaAndFlagsValue.0 is TelegramMediaStory || mediaAndFlagsValue.0 is WallpaperPreviewMedia {
|
|
var flags = mediaAndFlagsValue.1
|
|
flags.remove(.preferMediaInline)
|
|
mediaAndFlags = (mediaAndFlagsValue.0, flags)
|
|
}
|
|
}
|
|
|
|
var contentMediaAspectFilled = false
|
|
if let (_, flags) = mediaAndFlags {
|
|
contentMediaAspectFilled = flags.contains(.preferMediaAspectFilled)
|
|
}
|
|
var contentMediaInline = false
|
|
|
|
if let (media, flags) = mediaAndFlags {
|
|
contentMediaInline = flags.contains(.preferMediaInline)
|
|
|
|
if let file = media as? TelegramMediaFile {
|
|
if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 {
|
|
contentMediaValue = file
|
|
} else if file.isInstantVideo {
|
|
contentMediaValue = file
|
|
} else if file.isVideo {
|
|
contentMediaValue = file
|
|
} else if file.isSticker || file.isAnimatedSticker {
|
|
contentMediaValue = file
|
|
} else {
|
|
contentFileValue = file
|
|
}
|
|
|
|
if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) {
|
|
contentMediaAutomaticDownload = .full
|
|
} else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) {
|
|
contentMediaAutomaticDownload = .prefetch
|
|
}
|
|
|
|
if file.isAnimated {
|
|
contentMediaAutomaticPlayback = context.sharedContext.energyUsageSettings.autoplayGif
|
|
} else if file.isVideo && context.sharedContext.energyUsageSettings.autoplayVideo {
|
|
var willDownloadOrLocal = false
|
|
if case .full = contentMediaAutomaticDownload {
|
|
willDownloadOrLocal = true
|
|
} else {
|
|
willDownloadOrLocal = context.account.postbox.mediaBox.completedResourcePath(file.resource) != nil
|
|
}
|
|
if willDownloadOrLocal {
|
|
contentMediaAutomaticPlayback = true
|
|
contentMediaAspectFilled = true
|
|
}
|
|
}
|
|
} else if let _ = media as? TelegramMediaImage {
|
|
contentMediaValue = media
|
|
} else if let _ = media as? TelegramMediaWebFile {
|
|
contentMediaValue = media
|
|
} else if let _ = media as? WallpaperPreviewMedia {
|
|
contentMediaValue = media
|
|
} else if let _ = media as? TelegramMediaStory {
|
|
contentMediaValue = media
|
|
}
|
|
}
|
|
|
|
var maxWidth: CGFloat = .greatestFiniteMagnitude
|
|
|
|
let contentMediaContinueLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))?
|
|
let inlineMediaAndSize: (Media, CGSize)?
|
|
|
|
if let contentMediaValue {
|
|
if contentMediaInline {
|
|
contentMediaContinueLayout = nil
|
|
|
|
if let image = contentMediaValue as? TelegramMediaImage {
|
|
inlineMediaAndSize = (image, CGSize(width: 54.0, height: 54.0))
|
|
} else if let file = contentMediaValue as? TelegramMediaFile, !file.previewRepresentations.isEmpty {
|
|
inlineMediaAndSize = (file, CGSize(width: 54.0, height: 54.0))
|
|
} else {
|
|
inlineMediaAndSize = nil
|
|
}
|
|
} else {
|
|
let contentMode: InteractiveMediaNodeContentMode = contentMediaAspectFilled ? .aspectFill : .aspectFit
|
|
|
|
let (_, initialImageWidth, refineLayout) = makeContentMedia(
|
|
context,
|
|
presentationData,
|
|
presentationData.dateTimeFormat,
|
|
message, associatedData,
|
|
attributes,
|
|
contentMediaValue,
|
|
nil,
|
|
.full,
|
|
associatedData.automaticDownloadPeerType,
|
|
associatedData.automaticDownloadPeerId,
|
|
.constrained(CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)),
|
|
layoutConstants,
|
|
contentMode,
|
|
controllerInteraction.presentationContext
|
|
)
|
|
contentMediaContinueLayout = refineLayout
|
|
maxWidth = initialImageWidth + insets.left + insets.right
|
|
|
|
inlineMediaAndSize = nil
|
|
}
|
|
} else {
|
|
contentMediaContinueLayout = nil
|
|
inlineMediaAndSize = nil
|
|
}
|
|
|
|
let contentFileContinueLayout: ChatMessageInteractiveFileNode.ContinueLayout?
|
|
if let contentFileValue {
|
|
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: contentFileValue)
|
|
|
|
let (_, refineLayout) = makeContentFile(ChatMessageInteractiveFileNode.Arguments(
|
|
context: context,
|
|
presentationData: presentationData,
|
|
customTintColor: incoming ? mainColor : nil,
|
|
message: message,
|
|
topMessage: message,
|
|
associatedData: associatedData,
|
|
chatLocation: chatLocation,
|
|
attributes: attributes,
|
|
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
|
forcedIsEdited: false,
|
|
file: contentFileValue,
|
|
automaticDownload: automaticDownload,
|
|
incoming: incoming,
|
|
isRecentActions: false,
|
|
forcedResourceStatus: associatedData.forcedResourceStatus,
|
|
dateAndStatusType: nil,
|
|
displayReactions: false,
|
|
messageSelection: nil,
|
|
isAttachedContentBlock: true,
|
|
layoutConstants: layoutConstants,
|
|
constrainedSize: CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height),
|
|
controllerInteraction: controllerInteraction
|
|
))
|
|
contentFileContinueLayout = refineLayout
|
|
} else {
|
|
contentFileContinueLayout = nil
|
|
}
|
|
|
|
return (maxWidth, { constrainedSize, position in
|
|
enum ContentLayoutOrderItem {
|
|
case title
|
|
case subtitle
|
|
case text
|
|
case media
|
|
case file
|
|
case actionButton
|
|
}
|
|
var contentLayoutOrder: [ContentLayoutOrderItem] = []
|
|
|
|
if let title = title, !title.isEmpty {
|
|
contentLayoutOrder.append(.title)
|
|
}
|
|
if let subtitle = subtitle, !subtitle.string.isEmpty {
|
|
contentLayoutOrder.append(.subtitle)
|
|
}
|
|
if let text = text, !text.isEmpty {
|
|
contentLayoutOrder.append(.text)
|
|
}
|
|
if contentMediaContinueLayout != nil {
|
|
if let (_, flags) = mediaAndFlags {
|
|
if flags.contains(.titleBeforeMedia) {
|
|
if let index = contentLayoutOrder.firstIndex(of: .title) {
|
|
contentLayoutOrder.insert(.media, at: index + 1)
|
|
} else {
|
|
contentLayoutOrder.insert(.media, at: 0)
|
|
}
|
|
} else if flags.contains(.preferMediaBeforeText) {
|
|
contentLayoutOrder.insert(.media, at: 0)
|
|
} else {
|
|
contentLayoutOrder.append(.media)
|
|
}
|
|
} else {
|
|
contentLayoutOrder.append(.media)
|
|
}
|
|
}
|
|
if contentFileContinueLayout != nil {
|
|
contentLayoutOrder.append(.file)
|
|
}
|
|
if !isPreview, actionTitle != nil {
|
|
contentLayoutOrder.append(.actionButton)
|
|
}
|
|
|
|
var actualWidth: CGFloat = 0.0
|
|
|
|
let maxContentsWidth: CGFloat = constrainedSize.width - insets.left - insets.right
|
|
|
|
var titleLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
|
|
var subtitleLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
|
|
var textLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
|
|
|
|
var remainingCutoutHeight: CGFloat = 0.0
|
|
var cutoutWidth: CGFloat = 0.0
|
|
if let (_, inlineMediaSize) = inlineMediaAndSize {
|
|
remainingCutoutHeight = inlineMediaSize.height
|
|
cutoutWidth = inlineMediaSize.width + inlineMediaEdgeInset
|
|
}
|
|
for item in contentLayoutOrder {
|
|
switch item {
|
|
case .title:
|
|
if let title = title, !title.isEmpty {
|
|
var cutout: TextNodeCutout?
|
|
if remainingCutoutHeight > 0.0 {
|
|
cutout = TextNodeCutout(topRight: CGSize(width: cutoutWidth, height: remainingCutoutHeight))
|
|
}
|
|
|
|
let titleString = NSAttributedString(string: title, font: titleFont, textColor: mainColor)
|
|
let titleLayoutAndApplyValue = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets()))
|
|
titleLayoutAndApply = titleLayoutAndApplyValue
|
|
|
|
remainingCutoutHeight -= titleLayoutAndApplyValue.0.size.height
|
|
}
|
|
case .subtitle:
|
|
if let subtitle = subtitle, !subtitle.string.isEmpty {
|
|
var cutout: TextNodeCutout?
|
|
if remainingCutoutHeight > 0.0 {
|
|
cutout = TextNodeCutout(topRight: CGSize(width: cutoutWidth, height: remainingCutoutHeight))
|
|
}
|
|
|
|
let subtitleString = NSMutableAttributedString(attributedString: subtitle)
|
|
subtitleString.addAttribute(.foregroundColor, value: messageTheme.primaryTextColor, range: NSMakeRange(0, subtitle.length))
|
|
subtitleString.addAttribute(.font, value: titleFont, range: NSMakeRange(0, subtitle.length))
|
|
|
|
let subtitleLayoutAndApplyValue = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets()))
|
|
subtitleLayoutAndApply = subtitleLayoutAndApplyValue
|
|
|
|
remainingCutoutHeight -= subtitleLayoutAndApplyValue.0.size.height
|
|
}
|
|
case .text:
|
|
if let text = text, !text.isEmpty {
|
|
var cutout: TextNodeCutout?
|
|
if remainingCutoutHeight > 0.0 {
|
|
cutout = TextNodeCutout(topRight: CGSize(width: cutoutWidth, height: remainingCutoutHeight))
|
|
}
|
|
|
|
let textString = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: messageTheme.primaryTextColor, linkColor: incoming ? mainColor : messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil, adjustQuoteFontSize: true)
|
|
let textLayoutAndApplyValue = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets()))
|
|
textLayoutAndApply = textLayoutAndApplyValue
|
|
|
|
remainingCutoutHeight -= textLayoutAndApplyValue.0.size.height
|
|
}
|
|
case .media, .file, .actionButton:
|
|
break
|
|
}
|
|
}
|
|
|
|
if let (titleLayout, _) = titleLayoutAndApply {
|
|
actualWidth = max(actualWidth, titleLayout.size.width)
|
|
}
|
|
if let (subtitleLayout, _) = subtitleLayoutAndApply {
|
|
actualWidth = max(actualWidth, subtitleLayout.size.width)
|
|
}
|
|
if let (textLayout, _) = textLayoutAndApply {
|
|
actualWidth = max(actualWidth, textLayout.size.width)
|
|
}
|
|
|
|
let actionButtonMinWidthAndFinalizeLayout: (CGFloat, ((CGFloat, CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode)))?
|
|
if !isPreview, let actionTitle {
|
|
var buttonIconImage: UIImage?
|
|
var cornerIcon = false
|
|
|
|
if incoming {
|
|
if let actionIcon {
|
|
switch actionIcon {
|
|
case .instant:
|
|
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme.theme)!
|
|
case .link:
|
|
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkIncoming(presentationData.theme.theme)!
|
|
cornerIcon = true
|
|
}
|
|
}
|
|
} else {
|
|
if let actionIcon {
|
|
switch actionIcon {
|
|
case .instant:
|
|
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)!
|
|
case .link:
|
|
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkOutgoing(presentationData.theme.theme)!
|
|
cornerIcon = true
|
|
}
|
|
}
|
|
}
|
|
|
|
let (buttonWidth, continueLayout) = makeActionButtonLayout(
|
|
maxContentsWidth,
|
|
buttonIconImage,
|
|
cornerIcon,
|
|
actionTitle,
|
|
mainColor,
|
|
false,
|
|
message.adAttribute != nil
|
|
)
|
|
actionButtonMinWidthAndFinalizeLayout = (buttonWidth, continueLayout)
|
|
actualWidth = max(actualWidth, buttonWidth)
|
|
} else {
|
|
actionButtonMinWidthAndFinalizeLayout = nil
|
|
}
|
|
|
|
let contentMediaFinalizeLayout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))?
|
|
if let contentMediaContinueLayout {
|
|
let (refinedWidth, finalizeImageLayout) = contentMediaContinueLayout(CGSize(width: constrainedSize.width, height: constrainedSize.height), contentMediaAutomaticPlayback, true, ImageCorners(radius: 4.0))
|
|
actualWidth = max(actualWidth, refinedWidth)
|
|
contentMediaFinalizeLayout = finalizeImageLayout
|
|
} else {
|
|
contentMediaFinalizeLayout = nil
|
|
}
|
|
|
|
let contentFileFinalizeLayout: ChatMessageInteractiveFileNode.FinalizeLayout?
|
|
if let contentFileContinueLayout {
|
|
let (refinedWidth, finalizeFileLayout) = contentFileContinueLayout(CGSize(width: constrainedSize.width, height: constrainedSize.height))
|
|
actualWidth = max(actualWidth, refinedWidth)
|
|
contentFileFinalizeLayout = finalizeFileLayout
|
|
} else {
|
|
contentFileFinalizeLayout = nil
|
|
}
|
|
|
|
var edited = false
|
|
if attributes.updatingMedia != nil {
|
|
edited = true
|
|
}
|
|
var viewCount: Int?
|
|
var dateReplies = 0
|
|
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: associatedData.accountPeer, message: message)
|
|
if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) {
|
|
dateReactionsAndPeers = ([], [])
|
|
}
|
|
for attribute in 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 = chatLocation {
|
|
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info {
|
|
dateReplies = Int(attribute.count)
|
|
}
|
|
}
|
|
}
|
|
|
|
let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, associatedData: associatedData)
|
|
|
|
let statusType: ChatMessageDateAndStatusType
|
|
if incoming {
|
|
statusType = .BubbleIncoming
|
|
} else {
|
|
if message.flags.contains(.Failed) {
|
|
statusType = .BubbleOutgoing(.Failed)
|
|
} else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil {
|
|
statusType = .BubbleOutgoing(.Sending)
|
|
} else {
|
|
statusType = .BubbleOutgoing(.Sent(read: messageRead))
|
|
}
|
|
}
|
|
|
|
let maxStatusContentWidth: CGFloat = constrainedSize.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right
|
|
|
|
var trailingContentWidth: CGFloat?
|
|
if let _ = message.adAttribute, let (textLayout, _) = textLayoutAndApply {
|
|
if textLayout.hasRTL {
|
|
trailingContentWidth = 10000.0
|
|
} else {
|
|
trailingContentWidth = textLayout.trailingLineWidth
|
|
}
|
|
} else {
|
|
if !displayLine, let (actionButtonMinWidth, _) = actionButtonMinWidthAndFinalizeLayout {
|
|
trailingContentWidth = actionButtonMinWidth
|
|
}
|
|
}
|
|
|
|
var statusLayoutAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))?
|
|
if case let .linear(_, bottom) = position {
|
|
switch bottom {
|
|
case .None, .Neighbour(_, .footer, _):
|
|
let statusLayoutAndContinueValue = makeStatusLayout(ChatMessageDateAndStatusNode.Arguments(
|
|
context: context,
|
|
presentationData: presentationData,
|
|
edited: edited,
|
|
impressionCount: viewCount,
|
|
dateText: dateText,
|
|
type: statusType,
|
|
layoutInput: .trailingContent(
|
|
contentWidth: trailingContentWidth,
|
|
reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions), preferAdditionalInset: false)
|
|
),
|
|
constrainedSize: CGSize(width: maxStatusContentWidth, height: CGFloat.greatestFiniteMagnitude),
|
|
availableReactions: associatedData.availableReactions,
|
|
reactions: dateReactionsAndPeers.reactions,
|
|
reactionPeers: dateReactionsAndPeers.peers,
|
|
displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser,
|
|
replyCount: dateReplies,
|
|
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
|
hasAutoremove: message.isSelfExpiring,
|
|
canViewReactionList: canViewMessageReactionList(message: message),
|
|
animationCache: controllerInteraction.presentationContext.animationCache,
|
|
animationRenderer: controllerInteraction.presentationContext.animationRenderer
|
|
))
|
|
statusLayoutAndContinue = statusLayoutAndContinueValue
|
|
actualWidth = max(actualWidth, statusLayoutAndContinueValue.0)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
actualWidth += insets.left + insets.right
|
|
|
|
return (actualWidth, { resultingWidth in
|
|
let statusSizeAndApply = statusLayoutAndContinue?.1(resultingWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0)
|
|
|
|
let contentMediaSizeAndApply: (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)?
|
|
if let contentMediaFinalizeLayout {
|
|
let (size, apply) = contentMediaFinalizeLayout(resultingWidth - insets.left - insets.right)
|
|
contentMediaSizeAndApply = (size, apply)
|
|
} else {
|
|
contentMediaSizeAndApply = nil
|
|
}
|
|
|
|
let contentFileSizeAndApply: (CGSize, ChatMessageInteractiveFileNode.Apply)?
|
|
if let contentFileFinalizeLayout {
|
|
let (size, apply) = contentFileFinalizeLayout(resultingWidth - insets.left - insets.right)
|
|
contentFileSizeAndApply = (size, apply)
|
|
} else {
|
|
contentFileSizeAndApply = nil
|
|
}
|
|
|
|
let actionButtonSizeAndApply: ((CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode))?
|
|
if let (_, actionButtonFinalizeLayout) = actionButtonMinWidthAndFinalizeLayout {
|
|
let (size, apply) = actionButtonFinalizeLayout(resultingWidth - insets.left - insets.right, 36.0)
|
|
actionButtonSizeAndApply = (size, apply)
|
|
} else {
|
|
actionButtonSizeAndApply = nil
|
|
}
|
|
|
|
var actualSize = CGSize()
|
|
|
|
var backgroundInsets = UIEdgeInsets()
|
|
backgroundInsets.left += layoutConstants.text.bubbleInsets.left
|
|
backgroundInsets.right += layoutConstants.text.bubbleInsets.right
|
|
|
|
if case let .linear(top, _) = position {
|
|
switch top {
|
|
case .None:
|
|
actualSize.height += 11.0
|
|
backgroundInsets.top = actualSize.height
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
actualSize.width = resultingWidth
|
|
|
|
struct ContentDisplayOrderItem {
|
|
let item: ContentLayoutOrderItem
|
|
let offsetY: CGFloat
|
|
}
|
|
var contentDisplayOrder: [ContentDisplayOrderItem] = []
|
|
|
|
for i in 0 ..< contentLayoutOrder.count {
|
|
let item = contentLayoutOrder[i]
|
|
switch item {
|
|
case .title:
|
|
if let (titleLayout, _) = titleLayoutAndApply {
|
|
if i == 0 {
|
|
actualSize.height += textTopSpacing
|
|
} else if contentLayoutOrder[i - 1] == .media || contentLayoutOrder[i - 1] == .file {
|
|
actualSize.height += textContentMediaSpacing
|
|
}
|
|
|
|
contentDisplayOrder.append(ContentDisplayOrderItem(
|
|
item: item,
|
|
offsetY: actualSize.height
|
|
))
|
|
|
|
actualSize.height += titleLayout.size.height - titleLayout.insets.top - titleLayout.insets.bottom
|
|
}
|
|
case .subtitle:
|
|
if let (subtitleLayout, _) = subtitleLayoutAndApply {
|
|
if i == 0 {
|
|
actualSize.height += textTopSpacing
|
|
} else if contentLayoutOrder[i - 1] == .title {
|
|
actualSize.height += titleTextSpacing
|
|
} else if contentLayoutOrder[i - 1] == .media || contentLayoutOrder[i - 1] == .file {
|
|
actualSize.height += textContentMediaSpacing
|
|
}
|
|
|
|
contentDisplayOrder.append(ContentDisplayOrderItem(
|
|
item: item,
|
|
offsetY: actualSize.height
|
|
))
|
|
|
|
actualSize.height += subtitleLayout.size.height - subtitleLayout.insets.top - subtitleLayout.insets.bottom
|
|
}
|
|
case .text:
|
|
if let (textLayout, _) = textLayoutAndApply {
|
|
if i == 0 {
|
|
actualSize.height += textTopSpacing
|
|
} else if contentLayoutOrder[i - 1] == .title || contentLayoutOrder[i - 1] == .subtitle {
|
|
actualSize.height += titleTextSpacing
|
|
} else if contentLayoutOrder[i - 1] == .media || contentLayoutOrder[i - 1] == .file {
|
|
actualSize.height += textContentMediaSpacing
|
|
}
|
|
|
|
contentDisplayOrder.append(ContentDisplayOrderItem(
|
|
item: item,
|
|
offsetY: actualSize.height
|
|
))
|
|
|
|
actualSize.height += textLayout.size.height - textLayout.insets.top - textLayout.insets.bottom
|
|
}
|
|
case .media:
|
|
if let (contentMediaSize, _) = contentMediaSizeAndApply {
|
|
if i == 0 {
|
|
actualSize.height += contentMediaTopSpacing
|
|
} else if contentLayoutOrder[i - 1] == .title || contentLayoutOrder[i - 1] == .subtitle || contentLayoutOrder[i - 1] == .text {
|
|
actualSize.height += textContentMediaSpacing
|
|
}
|
|
|
|
contentDisplayOrder.append(ContentDisplayOrderItem(
|
|
item: item,
|
|
offsetY: actualSize.height
|
|
))
|
|
|
|
actualSize.height += contentMediaSize.height
|
|
}
|
|
case .file:
|
|
if let (contentFileSize, _) = contentFileSizeAndApply {
|
|
if i == 0 {
|
|
actualSize.height += contentMediaTopSpacing
|
|
} else if contentLayoutOrder[i - 1] == .title || contentLayoutOrder[i - 1] == .subtitle || contentLayoutOrder[i - 1] == .text {
|
|
actualSize.height += textContentMediaSpacing
|
|
}
|
|
|
|
contentDisplayOrder.append(ContentDisplayOrderItem(
|
|
item: item,
|
|
offsetY: actualSize.height
|
|
))
|
|
|
|
actualSize.height += contentFileSize.height
|
|
}
|
|
case .actionButton:
|
|
if let (actionButtonSize, _) = actionButtonSizeAndApply {
|
|
if i != 0 {
|
|
switch contentLayoutOrder[i - 1] {
|
|
case .title, .subtitle, .text:
|
|
actualSize.height += textButtonSpacing
|
|
case .media, .file:
|
|
actualSize.height += contentMediaButtonSpacing
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if let (_, inlineMediaSize) = inlineMediaAndSize {
|
|
if actualSize.height < insets.top + inlineMediaEdgeInset + inlineMediaSize.height + contentMediaButtonSpacing {
|
|
actualSize.height = insets.top + inlineMediaEdgeInset + inlineMediaSize.height + contentMediaButtonSpacing
|
|
}
|
|
}
|
|
|
|
contentDisplayOrder.append(ContentDisplayOrderItem(
|
|
item: item,
|
|
offsetY: actualSize.height
|
|
))
|
|
|
|
actualSize.height += actionButtonSize.height
|
|
}
|
|
}
|
|
}
|
|
|
|
if !contentLayoutOrder.isEmpty {
|
|
switch contentLayoutOrder[contentLayoutOrder.count - 1] {
|
|
case .title, .subtitle, .text:
|
|
actualSize.height += textBottomSpacing
|
|
|
|
if let (_, inlineMediaSize) = inlineMediaAndSize {
|
|
if actualSize.height < backgroundInsets.top + inlineMediaEdgeInset + inlineMediaSize.height + inlineMediaEdgeInset {
|
|
actualSize.height = backgroundInsets.top + inlineMediaEdgeInset + inlineMediaSize.height + inlineMediaEdgeInset
|
|
}
|
|
}
|
|
case .media, .file:
|
|
actualSize.height += contentMediaBottomSpacing
|
|
case .actionButton:
|
|
actualSize.height += buttonBottomSpacing
|
|
}
|
|
} else {
|
|
if let (_, inlineMediaSize) = inlineMediaAndSize {
|
|
if actualSize.height < backgroundInsets.top + inlineMediaEdgeInset + inlineMediaSize.height + inlineMediaEdgeInset {
|
|
actualSize.height = backgroundInsets.top + inlineMediaEdgeInset + inlineMediaSize.height + inlineMediaEdgeInset
|
|
}
|
|
}
|
|
}
|
|
|
|
if case let .linear(_, bottom) = position, let statusSizeAndApply {
|
|
switch bottom {
|
|
case .None, .Neighbour(_, .footer, _):
|
|
let bottomStatusContentHeight = statusBackgroundSpacing + statusSizeAndApply.0.height
|
|
actualSize.height += bottomStatusContentHeight
|
|
backgroundInsets.bottom += bottomStatusContentHeight
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
return (actualSize, { animation, synchronousLoads, applyInfo in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
self.context = context
|
|
self.message = message
|
|
self.media = mediaAndFlags?.0
|
|
self.theme = presentationData.theme
|
|
|
|
animation.animator.updateFrame(layer: self.transformContainer.layer, frame: CGRect(origin: CGPoint(), size: actualSize), completion: nil)
|
|
|
|
if displayLine {
|
|
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: actualSize.width - backgroundInsets.left - backgroundInsets.right, height: actualSize.height - backgroundInsets.top - backgroundInsets.bottom))
|
|
|
|
let backgroundView: MessageInlineBlockBackgroundView
|
|
if let current = self.backgroundView {
|
|
backgroundView = current
|
|
animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil)
|
|
backgroundView.update(size: backgroundFrame.size, isTransparent: false, primaryColor: mainColor, secondaryColor: secondaryColor, thirdColor: tertiaryColor, pattern: nil, animation: animation)
|
|
} else {
|
|
backgroundView = MessageInlineBlockBackgroundView()
|
|
self.backgroundView = backgroundView
|
|
backgroundView.frame = backgroundFrame
|
|
self.transformContainer.view.insertSubview(backgroundView, at: 0)
|
|
backgroundView.update(size: backgroundFrame.size, isTransparent: false, primaryColor: mainColor, secondaryColor: secondaryColor, thirdColor: tertiaryColor, pattern: nil, animation: .None)
|
|
}
|
|
} else {
|
|
if let backgroundView = self.backgroundView {
|
|
self.backgroundView = nil
|
|
backgroundView.removeFromSuperview()
|
|
}
|
|
}
|
|
|
|
if let (inlineMediaValue, inlineMediaSize) = inlineMediaAndSize {
|
|
var inlineMediaFrame = CGRect(origin: CGPoint(x: actualSize.width - insets.right - inlineMediaSize.width, y: backgroundInsets.top + inlineMediaEdgeInset), size: inlineMediaSize)
|
|
if contentLayoutOrder.isEmpty {
|
|
inlineMediaFrame.origin.x = insets.left
|
|
}
|
|
|
|
let inlineMedia: TransformImageNode
|
|
var updateMedia = false
|
|
if let current = self.inlineMedia {
|
|
inlineMedia = current
|
|
|
|
if let curentInlineMediaValue = self.inlineMediaValue {
|
|
updateMedia = !curentInlineMediaValue.isSemanticallyEqual(to: inlineMediaValue)
|
|
} else {
|
|
updateMedia = true
|
|
}
|
|
|
|
animation.animator.updateFrame(layer: inlineMedia.layer, frame: inlineMediaFrame, completion: nil)
|
|
} else {
|
|
inlineMedia = TransformImageNode()
|
|
inlineMedia.contentAnimations = .subsequentUpdates
|
|
self.inlineMedia = inlineMedia
|
|
self.transformContainer.addSubnode(inlineMedia)
|
|
|
|
inlineMedia.frame = inlineMediaFrame
|
|
|
|
updateMedia = true
|
|
|
|
inlineMedia.alpha = 0.0
|
|
animation.animator.updateAlpha(layer: inlineMedia.layer, alpha: 1.0, completion: nil)
|
|
animation.animator.animateScale(layer: inlineMedia.layer, from: 0.01, to: 1.0, completion: nil)
|
|
}
|
|
self.inlineMediaValue = inlineMediaValue
|
|
|
|
var fittedImageSize = inlineMediaSize
|
|
if let image = inlineMediaValue as? TelegramMediaImage {
|
|
if let dimensions = image.representations.last?.dimensions.cgSize {
|
|
fittedImageSize = dimensions.aspectFilled(inlineMediaSize)
|
|
}
|
|
} else if let file = inlineMediaValue as? TelegramMediaFile {
|
|
if let dimensions = file.dimensions?.cgSize {
|
|
fittedImageSize = dimensions.aspectFilled(inlineMediaSize)
|
|
}
|
|
}
|
|
|
|
if updateMedia {
|
|
let resolvedInlineMediaValue = inlineMediaValue
|
|
|
|
if let image = resolvedInlineMediaValue as? TelegramMediaImage {
|
|
let updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), placeholderColor: mainColor.withMultipliedAlpha(0.1))
|
|
inlineMedia.setSignal(updateInlineImageSignal)
|
|
} else if let file = resolvedInlineMediaValue as? TelegramMediaFile, let representation = file.previewRepresentations.last {
|
|
let updateInlineImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file), representation: representation)
|
|
inlineMedia.setSignal(updateInlineImageSignal)
|
|
}
|
|
}
|
|
|
|
inlineMedia.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: fittedImageSize, boundingSize: inlineMediaSize, intrinsicInsets: UIEdgeInsets(), emptyColor: mainColor.withMultipliedAlpha(0.1)))()
|
|
} else {
|
|
if let inlineMedia = self.inlineMedia {
|
|
self.inlineMedia = nil
|
|
|
|
let inlineMediaFrame = CGRect(origin: CGPoint(x: actualSize.width - insets.right - inlineMedia.bounds.width, y: backgroundInsets.top + inlineMediaEdgeInset), size: inlineMedia.bounds.size)
|
|
animation.animator.updateFrame(layer: inlineMedia.layer, frame: inlineMediaFrame, completion: nil)
|
|
animation.animator.updateAlpha(layer: inlineMedia.layer, alpha: 0.0, completion: nil)
|
|
animation.animator.updateScale(layer: inlineMedia.layer, scale: 0.01, completion: { [weak inlineMedia] _ in
|
|
inlineMedia?.removeFromSupernode()
|
|
})
|
|
}
|
|
}
|
|
|
|
if let item = contentDisplayOrder.first(where: { $0.item == .title }), let (titleLayout, titleApply) = titleLayoutAndApply {
|
|
let title = titleApply(TextNodeWithEntities.Arguments(
|
|
context: context,
|
|
cache: animationCache,
|
|
renderer: animationRenderer,
|
|
placeholderColor: messageTheme.mediaPlaceholderColor,
|
|
attemptSynchronous: synchronousLoads
|
|
))
|
|
|
|
let titleFrame = CGRect(origin: CGPoint(x: -titleLayout.insets.left + insets.left, y: -titleLayout.insets.top + item.offsetY), size: titleLayout.size)
|
|
|
|
if self.title !== title {
|
|
self.title?.textNode.removeFromSupernode()
|
|
self.title = title
|
|
title.textNode.layer.anchorPoint = CGPoint()
|
|
self.transformContainer.addSubnode(title.textNode)
|
|
|
|
title.textNode.frame = titleFrame
|
|
} else {
|
|
title.textNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
|
animation.animator.updatePosition(layer: title.textNode.layer, position: titleFrame.origin, completion: nil)
|
|
}
|
|
} else {
|
|
if let title = self.title {
|
|
self.title = nil
|
|
title.textNode.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
if let item = contentDisplayOrder.first(where: { $0.item == .subtitle }), let (subtitleLayout, subtitleApply) = subtitleLayoutAndApply {
|
|
let subtitle = subtitleApply(TextNodeWithEntities.Arguments(
|
|
context: context,
|
|
cache: animationCache,
|
|
renderer: animationRenderer,
|
|
placeholderColor: messageTheme.mediaPlaceholderColor,
|
|
attemptSynchronous: synchronousLoads
|
|
))
|
|
|
|
let subtitleFrame = CGRect(origin: CGPoint(x: -subtitleLayout.insets.left + insets.left, y: -subtitleLayout.insets.top + item.offsetY), size: subtitleLayout.size)
|
|
|
|
if self.subtitle !== subtitle {
|
|
self.subtitle?.textNode.removeFromSupernode()
|
|
self.subtitle = subtitle
|
|
subtitle.textNode.layer.anchorPoint = CGPoint()
|
|
self.transformContainer.addSubnode(subtitle.textNode)
|
|
|
|
subtitle.textNode.frame = subtitleFrame
|
|
} else {
|
|
subtitle.textNode.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
|
|
animation.animator.updatePosition(layer: subtitle.textNode.layer, position: subtitleFrame.origin, completion: nil)
|
|
}
|
|
} else {
|
|
if let subtitle = self.subtitle {
|
|
self.subtitle = nil
|
|
subtitle.textNode.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
if let item = contentDisplayOrder.first(where: { $0.item == .text }), let (textLayout, textApply) = textLayoutAndApply {
|
|
let text = textApply(TextNodeWithEntities.Arguments(
|
|
context: context,
|
|
cache: animationCache,
|
|
renderer: animationRenderer,
|
|
placeholderColor: messageTheme.mediaPlaceholderColor,
|
|
attemptSynchronous: synchronousLoads
|
|
))
|
|
|
|
let textFrame = CGRect(origin: CGPoint(x: -textLayout.insets.left + insets.left, y: -textLayout.insets.top + item.offsetY), size: textLayout.size)
|
|
|
|
if self.text !== text {
|
|
self.text?.textNode.removeFromSupernode()
|
|
self.text = text
|
|
text.textNode.layer.anchorPoint = CGPoint()
|
|
self.transformContainer.addSubnode(text.textNode)
|
|
|
|
text.textNode.frame = textFrame
|
|
} else {
|
|
text.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
|
animation.animator.updatePosition(layer: text.textNode.layer, position: textFrame.origin, completion: nil)
|
|
}
|
|
} else {
|
|
if let text = self.text {
|
|
self.text = nil
|
|
text.textNode.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
if let item = contentDisplayOrder.first(where: { $0.item == .media }), let (contentMediaSize, contentMediaApply) = contentMediaSizeAndApply {
|
|
let contentMediaFrame = CGRect(origin: CGPoint(x: insets.left, y: item.offsetY), size: contentMediaSize)
|
|
|
|
let contentMedia = contentMediaApply(animation, synchronousLoads)
|
|
if self.contentMedia !== contentMedia {
|
|
self.contentMedia?.removeFromSupernode()
|
|
self.contentMedia = contentMedia
|
|
|
|
contentMedia.activateLocalContent = { [weak self] mode in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.openMedia?(mode)
|
|
}
|
|
contentMedia.updateMessageReaction = { [weak controllerInteraction] message, value in
|
|
guard let controllerInteraction else {
|
|
return
|
|
}
|
|
controllerInteraction.updateMessageReaction(message, value)
|
|
}
|
|
contentMedia.visibility = self.visibility != .none
|
|
|
|
self.transformContainer.addSubnode(contentMedia)
|
|
|
|
contentMedia.frame = contentMediaFrame
|
|
|
|
contentMedia.alpha = 0.0
|
|
animation.animator.updateAlpha(layer: contentMedia.layer, alpha: 1.0, completion: nil)
|
|
animation.animator.animateScale(layer: contentMedia.layer, from: 0.01, to: 1.0, completion: nil)
|
|
} else {
|
|
animation.animator.updateFrame(layer: contentMedia.layer, frame: contentMediaFrame, completion: nil)
|
|
}
|
|
} else {
|
|
if let contentMedia = self.contentMedia {
|
|
self.contentMedia = nil
|
|
|
|
animation.animator.updateAlpha(layer: contentMedia.layer, alpha: 0.0, completion: nil)
|
|
animation.animator.updateScale(layer: contentMedia.layer, scale: 0.01, completion: { [weak contentMedia] _ in
|
|
contentMedia?.removeFromSupernode()
|
|
})
|
|
}
|
|
}
|
|
|
|
if let item = contentDisplayOrder.first(where: { $0.item == .file }), let (contentFileSize, contentFileApply) = contentFileSizeAndApply {
|
|
let contentFileFrame = CGRect(origin: CGPoint(x: insets.left, y: item.offsetY), size: contentFileSize)
|
|
|
|
let contentFile = contentFileApply(synchronousLoads, animation, applyInfo)
|
|
if self.contentFile !== contentFile {
|
|
self.contentFile?.removeFromSupernode()
|
|
self.contentFile = contentFile
|
|
|
|
contentFile.activateLocalContent = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.openMedia?(.default)
|
|
}
|
|
contentFile.visibility = self.visibility != .none
|
|
|
|
self.transformContainer.addSubnode(contentFile)
|
|
|
|
contentFile.frame = contentFileFrame
|
|
|
|
contentFile.alpha = 0.0
|
|
animation.animator.updateAlpha(layer: contentFile.layer, alpha: 1.0, completion: nil)
|
|
animation.animator.animateScale(layer: contentFile.layer, from: 0.01, to: 1.0, completion: nil)
|
|
} else {
|
|
animation.animator.updateFrame(layer: contentFile.layer, frame: contentFileFrame, completion: nil)
|
|
}
|
|
} else {
|
|
if let contentFile = self.contentFile {
|
|
self.contentFile = nil
|
|
|
|
animation.animator.updateAlpha(layer: contentFile.layer, alpha: 0.0, completion: nil)
|
|
animation.animator.updateScale(layer: contentFile.layer, scale: 0.01, completion: { [weak contentFile] _ in
|
|
contentFile?.removeFromSupernode()
|
|
})
|
|
}
|
|
}
|
|
|
|
if let item = contentDisplayOrder.first(where: { $0.item == .actionButton }), let (actionButtonSize, actionButtonApply) = actionButtonSizeAndApply {
|
|
var actionButtonFrame = CGRect(origin: CGPoint(x: insets.left, y: item.offsetY), size: actionButtonSize)
|
|
if let _ = message.adAttribute, let statusSizeAndApply {
|
|
actionButtonFrame.origin.y += statusSizeAndApply.0.height
|
|
}
|
|
|
|
let actionButton = actionButtonApply(animation)
|
|
|
|
if self.actionButton !== actionButton {
|
|
self.actionButton?.removeFromSupernode()
|
|
self.actionButton = actionButton
|
|
self.transformContainer.addSubnode(actionButton)
|
|
actionButton.frame = actionButtonFrame
|
|
|
|
actionButton.pressed = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.activateAction?()
|
|
}
|
|
} else {
|
|
animation.animator.updateFrame(layer: actionButton.layer, frame: actionButtonFrame, completion: nil)
|
|
}
|
|
|
|
if let _ = message.adAttribute {
|
|
|
|
} else {
|
|
let separatorFrame = CGRect(origin: CGPoint(x: actionButtonFrame.minX, y: actionButtonFrame.minY - 1.0), size: CGSize(width: actionButtonFrame.width, height: UIScreenPixel))
|
|
|
|
let actionButtonSeparator: SimpleLayer
|
|
if let current = self.actionButtonSeparator {
|
|
actionButtonSeparator = current
|
|
animation.animator.updateFrame(layer: actionButtonSeparator, frame: separatorFrame, completion: nil)
|
|
} else {
|
|
actionButtonSeparator = SimpleLayer()
|
|
self.actionButtonSeparator = actionButtonSeparator
|
|
self.layer.addSublayer(actionButtonSeparator)
|
|
actionButtonSeparator.frame = separatorFrame
|
|
}
|
|
|
|
actionButtonSeparator.backgroundColor = mainColor.withMultipliedAlpha(0.2).cgColor
|
|
}
|
|
} else {
|
|
if let actionButton = self.actionButton {
|
|
self.actionButton = nil
|
|
actionButton.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
if self.actionButton == nil, let actionButtonSeparator = self.actionButtonSeparator {
|
|
self.actionButtonSeparator = nil
|
|
actionButtonSeparator.removeFromSuperlayer()
|
|
}
|
|
|
|
if let statusSizeAndApply {
|
|
var statusFrame = CGRect(origin: CGPoint(x: actualSize.width - backgroundInsets.right - statusSizeAndApply.0.width, y: actualSize.height - layoutConstants.text.bubbleInsets.bottom - statusSizeAndApply.0.height), size: statusSizeAndApply.0)
|
|
if let _ = message.adAttribute, let (actionButtonSize, _) = actionButtonSizeAndApply {
|
|
statusFrame.origin.y -= actionButtonSize.height + statusBackgroundSpacing
|
|
}
|
|
|
|
let statusNode = statusSizeAndApply.1(self.statusNode == nil ? .None : animation)
|
|
if self.statusNode !== statusNode {
|
|
self.statusNode?.removeFromSupernode()
|
|
self.statusNode = statusNode
|
|
self.addSubnode(statusNode)
|
|
|
|
statusNode.reactionSelected = { [weak self] value in
|
|
guard let self, let message = self.message else {
|
|
return
|
|
}
|
|
controllerInteraction.updateMessageReaction(message, .reaction(value))
|
|
}
|
|
|
|
statusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in
|
|
guard let self, let message = self.message else {
|
|
gesture?.cancel()
|
|
return
|
|
}
|
|
controllerInteraction.openMessageReactionContextMenu(message, sourceNode, gesture, value)
|
|
}
|
|
|
|
statusNode.frame = statusFrame
|
|
} else {
|
|
animation.animator.updateFrame(layer: statusNode.layer, frame: statusFrame, completion: nil)
|
|
}
|
|
} else if let statusNode = self.statusNode {
|
|
self.statusNode = nil
|
|
statusNode.removeFromSupernode()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
/*var horizontalInsets = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0)
|
|
if displayLine {
|
|
horizontalInsets.left += 10.0
|
|
horizontalInsets.right += 9.0
|
|
}
|
|
|
|
var titleBeforeMedia = false
|
|
var preferMediaBeforeText = false
|
|
var preferMediaAspectFilled = false
|
|
if let (_, flags) = mediaAndFlags {
|
|
preferMediaBeforeText = flags.contains(.preferMediaBeforeText)
|
|
preferMediaAspectFilled = flags.contains(.preferMediaAspectFilled)
|
|
titleBeforeMedia = flags.contains(.titleBeforeMedia)
|
|
}
|
|
|
|
var contentMode: InteractiveMediaNodeContentMode = preferMediaAspectFilled ? .aspectFill : .aspectFit
|
|
|
|
var edited = false
|
|
if attributes.updatingMedia != nil {
|
|
edited = true
|
|
}
|
|
var viewCount: Int?
|
|
var dateReplies = 0
|
|
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: associatedData.accountPeer, message: message)
|
|
if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) {
|
|
dateReactionsAndPeers = ([], [])
|
|
}
|
|
for attribute in 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 = chatLocation {
|
|
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info {
|
|
dateReplies = Int(attribute.count)
|
|
}
|
|
}
|
|
}
|
|
|
|
let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, associatedData: associatedData)
|
|
|
|
var webpageGalleryMediaCount: Int?
|
|
for media in message.media {
|
|
if let media = media as? TelegramMediaWebpage {
|
|
if case let .Loaded(content) = media.content, let instantPage = content.instantPage, let image = content.image {
|
|
switch instantPageType(of: content) {
|
|
case .album:
|
|
let count = instantPageGalleryMedia(webpageId: media.webpageId, page: instantPage, galleryMedia: image).count
|
|
if count > 1 {
|
|
webpageGalleryMediaCount = count
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var textString: NSAttributedString?
|
|
var inlineImageDimensions: CGSize?
|
|
var inlineImageSize: CGSize?
|
|
var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
|
var textCutout = TextNodeCutout()
|
|
var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude
|
|
var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))?
|
|
var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode)))?
|
|
|
|
var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode)?
|
|
|
|
let topTitleString = NSMutableAttributedString()
|
|
|
|
let string = NSMutableAttributedString()
|
|
var notEmpty = false
|
|
|
|
let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing
|
|
|
|
if let title = title, !title.isEmpty {
|
|
if titleBeforeMedia {
|
|
topTitleString.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor))
|
|
} else {
|
|
string.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor))
|
|
notEmpty = true
|
|
}
|
|
}
|
|
|
|
if let subtitle = subtitle, subtitle.length > 0 {
|
|
if notEmpty {
|
|
string.append(NSAttributedString(string: "\n", font: textFont, textColor: messageTheme.primaryTextColor))
|
|
}
|
|
let updatedSubtitle = NSMutableAttributedString()
|
|
updatedSubtitle.append(subtitle)
|
|
updatedSubtitle.addAttribute(.foregroundColor, value: messageTheme.primaryTextColor, range: NSMakeRange(0, subtitle.length))
|
|
updatedSubtitle.addAttribute(.font, value: titleFont, range: NSMakeRange(0, subtitle.length))
|
|
string.append(updatedSubtitle)
|
|
notEmpty = true
|
|
}
|
|
|
|
if let text = text, !text.isEmpty {
|
|
if notEmpty {
|
|
string.append(NSAttributedString(string: "\n", font: textFont, textColor: messageTheme.primaryTextColor))
|
|
}
|
|
if let entities = entities {
|
|
string.append(stringWithAppliedEntities(text, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil, adjustQuoteFontSize: true))
|
|
} else {
|
|
string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: messageTheme.primaryTextColor))
|
|
}
|
|
notEmpty = true
|
|
}
|
|
|
|
textString = string
|
|
if string.length > 1000 {
|
|
textString = string.attributedSubstring(from: NSMakeRange(0, 1000))
|
|
}
|
|
|
|
var isReplyThread = false
|
|
if case .replyThread = chatLocation {
|
|
isReplyThread = true
|
|
}
|
|
|
|
var skipStandardStatus = false
|
|
var isImage = false
|
|
var isFile = false
|
|
|
|
var automaticPlayback = false
|
|
|
|
var textStatusType: ChatMessageDateAndStatusType?
|
|
var imageStatusType: ChatMessageDateAndStatusType?
|
|
var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent?
|
|
|
|
if let (media, flags) = mediaAndFlags {
|
|
if let file = media as? TelegramMediaFile {
|
|
if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 {
|
|
isImage = true
|
|
} else if file.isInstantVideo {
|
|
isImage = true
|
|
} else if file.isVideo {
|
|
isImage = true
|
|
} else if file.isSticker || file.isAnimatedSticker {
|
|
isImage = true
|
|
} else {
|
|
isFile = true
|
|
}
|
|
} else if let _ = media as? TelegramMediaImage {
|
|
if !flags.contains(.preferMediaInline) {
|
|
isImage = true
|
|
}
|
|
} else if let _ = media as? TelegramMediaWebFile {
|
|
isImage = true
|
|
} else if let _ = media as? WallpaperPreviewMedia {
|
|
isImage = true
|
|
} else if let _ = media as? TelegramMediaStory {
|
|
isImage = true
|
|
}
|
|
}
|
|
|
|
if preferMediaBeforeText, let textString, textString.length != 0 {
|
|
isImage = false
|
|
}
|
|
|
|
var statusInText = !isImage
|
|
if let textString {
|
|
if textString.length == 0 {
|
|
statusInText = false
|
|
}
|
|
} else {
|
|
statusInText = false
|
|
}
|
|
|
|
switch preparePosition {
|
|
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
|
if let count = webpageGalleryMediaCount {
|
|
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").string), iconName: nil)
|
|
skipStandardStatus = isImage
|
|
} else if let mediaBadge = mediaBadge {
|
|
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge), iconName: nil)
|
|
} else {
|
|
skipStandardStatus = isFile
|
|
}
|
|
|
|
if !skipStandardStatus {
|
|
if incoming {
|
|
if isImage {
|
|
imageStatusType = .ImageIncoming
|
|
} else {
|
|
textStatusType = .BubbleIncoming
|
|
}
|
|
} else {
|
|
if message.flags.contains(.Failed) {
|
|
if isImage {
|
|
imageStatusType = .ImageOutgoing(.Failed)
|
|
} else {
|
|
textStatusType = .BubbleOutgoing(.Failed)
|
|
}
|
|
} else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil {
|
|
if isImage {
|
|
imageStatusType = .ImageOutgoing(.Sending)
|
|
} else {
|
|
textStatusType = .BubbleOutgoing(.Sending)
|
|
}
|
|
} else {
|
|
if isImage {
|
|
imageStatusType = .ImageOutgoing(.Sent(read: messageRead))
|
|
} else {
|
|
textStatusType = .BubbleOutgoing(.Sent(read: messageRead))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
|
|
let imageDateAndStatus = imageStatusType.flatMap { statusType -> ChatMessageDateAndStatus in
|
|
ChatMessageDateAndStatus(
|
|
type: statusType,
|
|
edited: edited,
|
|
viewCount: viewCount,
|
|
dateReactions: dateReactionsAndPeers.reactions,
|
|
dateReactionPeers: dateReactionsAndPeers.peers,
|
|
dateReplies: dateReplies,
|
|
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
|
dateText: dateText
|
|
)
|
|
}
|
|
|
|
if let (media, flags) = mediaAndFlags {
|
|
if let file = media as? TelegramMediaFile {
|
|
if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 {
|
|
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
refineContentImageLayout = refineLayout
|
|
} else if file.isInstantVideo {
|
|
let displaySize = CGSize(width: 212.0, height: 212.0)
|
|
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
|
|
let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, topMessage: message, read: messageRead, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, displaySize, displaySize, 0.0, .bubble, automaticDownload, 0.0)
|
|
initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight
|
|
contentInstantVideoSizeAndApply = (videoLayout, apply)
|
|
} else if file.isVideo {
|
|
var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none
|
|
|
|
if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) {
|
|
automaticDownload = .full
|
|
} else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) {
|
|
automaticDownload = .prefetch
|
|
}
|
|
if file.isAnimated {
|
|
automaticPlayback = context.sharedContext.energyUsageSettings.autoplayGif
|
|
} else if file.isVideo && context.sharedContext.energyUsageSettings.autoplayVideo {
|
|
var willDownloadOrLocal = false
|
|
if case .full = automaticDownload {
|
|
willDownloadOrLocal = true
|
|
} else {
|
|
willDownloadOrLocal = context.account.postbox.mediaBox.completedResourcePath(file.resource) != nil
|
|
}
|
|
if willDownloadOrLocal {
|
|
automaticPlayback = true
|
|
contentMode = .aspectFill
|
|
}
|
|
}
|
|
|
|
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
refineContentImageLayout = refineLayout
|
|
} else if file.isSticker || file.isAnimatedSticker {
|
|
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
|
|
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
refineContentImageLayout = refineLayout
|
|
} else {
|
|
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
|
|
|
|
let statusType: ChatMessageDateAndStatusType
|
|
if incoming {
|
|
statusType = .BubbleIncoming
|
|
} else {
|
|
if message.flags.contains(.Failed) {
|
|
statusType = .BubbleOutgoing(.Failed)
|
|
} else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil {
|
|
statusType = .BubbleOutgoing(.Sending)
|
|
} else {
|
|
statusType = .BubbleOutgoing(.Sent(read: messageRead))
|
|
}
|
|
}
|
|
|
|
let (_, refineLayout) = contentFileLayout(ChatMessageInteractiveFileNode.Arguments(
|
|
context: context,
|
|
presentationData: presentationData,
|
|
message: message,
|
|
topMessage: message,
|
|
associatedData: associatedData,
|
|
chatLocation: chatLocation,
|
|
attributes: attributes,
|
|
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
|
forcedIsEdited: false,
|
|
file: file,
|
|
automaticDownload: automaticDownload,
|
|
incoming: incoming,
|
|
isRecentActions: false,
|
|
forcedResourceStatus: associatedData.forcedResourceStatus,
|
|
dateAndStatusType: statusType,
|
|
displayReactions: false,
|
|
messageSelection: nil,
|
|
layoutConstants: layoutConstants,
|
|
constrainedSize: CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height),
|
|
controllerInteraction: controllerInteraction
|
|
))
|
|
refineContentFileLayout = refineLayout
|
|
}
|
|
} else if let image = media as? TelegramMediaImage {
|
|
if !flags.contains(.preferMediaInline) {
|
|
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image)
|
|
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
refineContentImageLayout = refineLayout
|
|
} else if let dimensions = largestImageRepresentation(image.representations)?.dimensions {
|
|
inlineImageDimensions = dimensions.cgSize
|
|
|
|
if image != currentImage || !currentMediaIsInline {
|
|
updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image))
|
|
}
|
|
}
|
|
} else if let image = media as? TelegramMediaWebFile {
|
|
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image)
|
|
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
refineContentImageLayout = refineLayout
|
|
} else if let wallpaper = media as? WallpaperPreviewMedia {
|
|
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, wallpaper, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
refineContentImageLayout = refineLayout
|
|
if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme {
|
|
skipStandardStatus = true
|
|
}
|
|
} else if let story = media as? TelegramMediaStory {
|
|
var media: Media?
|
|
if let storyValue = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyValue {
|
|
media = item.media
|
|
}
|
|
|
|
var automaticDownload = false
|
|
if let media {
|
|
automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: media)
|
|
}
|
|
|
|
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, story, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
refineContentImageLayout = refineLayout
|
|
}
|
|
}
|
|
|
|
if let _ = inlineImageDimensions {
|
|
inlineImageSize = CGSize(width: 53.0, height: 53.0)
|
|
|
|
if let inlineImageSize = inlineImageSize {
|
|
textCutout.topRight = CGSize(width: inlineImageSize.width + 10.0, height: inlineImageSize.height + 10.0)
|
|
}
|
|
}
|
|
|
|
return (initialWidth, { constrainedSize, position in
|
|
var insets = UIEdgeInsets(top: 0.0, left: horizontalInsets.left, bottom: 0.0, right: horizontalInsets.right)
|
|
|
|
switch position {
|
|
case let .linear(topNeighbor, bottomNeighbor):
|
|
switch topNeighbor {
|
|
case .None:
|
|
insets.top += 10.0
|
|
default:
|
|
break
|
|
}
|
|
switch bottomNeighbor {
|
|
case .None:
|
|
insets.bottom += 12.0
|
|
default:
|
|
insets.bottom += 0.0
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
|
|
let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom)
|
|
|
|
var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge?
|
|
if let _ = additionalImageBadgeContent {
|
|
updatedAdditionalImageBadge = currentAdditionalImageBadgeNode ?? ChatMessageInteractiveMediaBadge()
|
|
}
|
|
|
|
let upatedTextCutout = textCutout
|
|
|
|
|
|
let (topTitleLayout, topTitleApply) = topTitleAsyncLayout(TextNodeLayoutArguments(attributedString: topTitleString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: upatedTextCutout, insets: UIEdgeInsets()))
|
|
|
|
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
|
|
if statusInText, let textStatusType = textStatusType {
|
|
let trailingContentWidth: CGFloat
|
|
if textLayout.hasRTL {
|
|
trailingContentWidth = 10000.0
|
|
} else {
|
|
trailingContentWidth = textLayout.trailingLineWidth
|
|
}
|
|
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
|
|
context: context,
|
|
presentationData: presentationData,
|
|
edited: edited,
|
|
impressionCount: viewCount,
|
|
dateText: dateText,
|
|
type: textStatusType,
|
|
layoutInput: .trailingContent(contentWidth: trailingContentWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
|
|
constrainedSize: textConstrainedSize,
|
|
availableReactions: associatedData.availableReactions,
|
|
reactions: dateReactionsAndPeers.reactions,
|
|
reactionPeers: dateReactionsAndPeers.peers,
|
|
displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser,
|
|
replyCount: dateReplies,
|
|
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
|
hasAutoremove: message.isSelfExpiring,
|
|
canViewReactionList: canViewMessageReactionList(message: message),
|
|
animationCache: controllerInteraction.presentationContext.animationCache,
|
|
animationRenderer: controllerInteraction.presentationContext.animationRenderer
|
|
))
|
|
}
|
|
let _ = statusSuggestedWidthAndContinue
|
|
|
|
var textFrame = CGRect(origin: CGPoint(), size: textLayout.size)
|
|
|
|
textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top)
|
|
|
|
let mainColor: UIColor
|
|
if !incoming {
|
|
mainColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
|
} else {
|
|
var authorNameColor: UIColor?
|
|
let author = message.author
|
|
if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), author?.id.namespace == Namespaces.Peer.CloudUser {
|
|
authorNameColor = author.flatMap { chatMessagePeerIdColors[Int(clamping: $0.id.id._internalGetInt64Value() % 7)] }
|
|
if let rawAuthorNameColor = authorNameColor {
|
|
var dimColors = false
|
|
switch presentationData.theme.theme.name {
|
|
case .builtin(.nightAccent), .builtin(.night):
|
|
dimColors = true
|
|
default:
|
|
break
|
|
}
|
|
if dimColors {
|
|
var hue: CGFloat = 0.0
|
|
var saturation: CGFloat = 0.0
|
|
var brightness: CGFloat = 0.0
|
|
rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil)
|
|
authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
if let authorNameColor {
|
|
mainColor = authorNameColor
|
|
} else {
|
|
mainColor = presentationData.theme.theme.chat.message.incoming.accentTextColor
|
|
}
|
|
}
|
|
|
|
var boundingSize = textFrame.size
|
|
if titleBeforeMedia {
|
|
boundingSize.height += topTitleLayout.size.height + 4.0
|
|
boundingSize.width = max(boundingSize.width, topTitleLayout.size.width)
|
|
}
|
|
if let inlineImageSize = inlineImageSize {
|
|
if boundingSize.height < inlineImageSize.height {
|
|
boundingSize.height = inlineImageSize.height
|
|
}
|
|
}
|
|
|
|
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
|
|
boundingSize.width = max(boundingSize.width, statusSuggestedWidthAndContinue.0)
|
|
}
|
|
|
|
var finalizeContentImageLayout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))?
|
|
if let refineContentImageLayout = refineContentImageLayout {
|
|
let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize, automaticPlayback, true, ImageCorners(radius: 4.0))
|
|
finalizeContentImageLayout = finalizeImageLayout
|
|
|
|
boundingSize.width = max(boundingSize.width, refinedWidth)
|
|
}
|
|
var finalizeContentFileLayout: ((CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode))?
|
|
if let refineContentFileLayout = refineContentFileLayout {
|
|
let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize)
|
|
finalizeContentFileLayout = finalizeFileLayout
|
|
|
|
boundingSize.width = max(boundingSize.width, refinedWidth)
|
|
}
|
|
|
|
if let (videoLayout, _) = contentInstantVideoSizeAndApply {
|
|
boundingSize.width = max(boundingSize.width, videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight)
|
|
}
|
|
|
|
var imageApply: (() -> Void)?
|
|
if let inlineImageSize = inlineImageSize, let inlineImageDimensions = inlineImageDimensions {
|
|
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0))
|
|
let arguments = TransformImageArguments(corners: imageCorners, imageSize: inlineImageDimensions.aspectFilled(inlineImageSize), boundingSize: inlineImageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: incoming ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor)
|
|
imageApply = imageLayout(arguments)
|
|
}
|
|
|
|
var continueActionButtonLayout: ((CGFloat, CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode))?
|
|
if let actionTitle = actionTitle, !isPreview {
|
|
var buttonIconImage: UIImage?
|
|
var buttonHighlightedIconImage: UIImage?
|
|
var cornerIcon = false
|
|
let titleColor: UIColor
|
|
let titleHighlightedColor: UIColor
|
|
if incoming {
|
|
if let actionIcon {
|
|
switch actionIcon {
|
|
case .instant:
|
|
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme.theme)!
|
|
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
|
case .link:
|
|
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkIncoming(presentationData.theme.theme)!
|
|
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
|
cornerIcon = true
|
|
}
|
|
}
|
|
titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor
|
|
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty)
|
|
titleHighlightedColor = bubbleColor.fill[0]
|
|
} else {
|
|
if let actionIcon {
|
|
switch actionIcon {
|
|
case .instant:
|
|
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)!
|
|
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
|
case .link:
|
|
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkOutgoing(presentationData.theme.theme)!
|
|
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
|
cornerIcon = true
|
|
}
|
|
}
|
|
titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
|
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty)
|
|
titleHighlightedColor = bubbleColor.fill[0]
|
|
}
|
|
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, nil, buttonIconImage, buttonHighlightedIconImage, cornerIcon, actionTitle, titleColor, titleHighlightedColor, false)
|
|
boundingSize.width = max(buttonWidth, boundingSize.width)
|
|
continueActionButtonLayout = continueLayout
|
|
}
|
|
|
|
boundingSize.width += insets.left + insets.right
|
|
boundingSize.height += insets.top + insets.bottom
|
|
|
|
return (boundingSize.width, { boundingWidth in
|
|
var adjustedBoundingSize = boundingSize
|
|
|
|
var imageFrame: CGRect?
|
|
if let inlineImageSize = inlineImageSize {
|
|
imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right + 4.0, y: 0.0), size: inlineImageSize)
|
|
}
|
|
|
|
var contentImageSizeAndApply: (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)?
|
|
if let finalizeContentImageLayout = finalizeContentImageLayout {
|
|
let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right)
|
|
contentImageSizeAndApply = (size, apply)
|
|
|
|
var imageHeightAddition = size.height
|
|
if textFrame.size.height > CGFloat.ulpOfOne {
|
|
imageHeightAddition += 2.0
|
|
}
|
|
|
|
adjustedBoundingSize.height += imageHeightAddition + 7.0
|
|
}
|
|
|
|
var contentFileSizeAndApply: (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode)?
|
|
if let finalizeContentFileLayout = finalizeContentFileLayout {
|
|
let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right)
|
|
contentFileSizeAndApply = (size, apply)
|
|
|
|
var imageHeightAddition = size.height + 6.0
|
|
if textFrame.size.height > CGFloat.ulpOfOne {
|
|
imageHeightAddition += 6.0
|
|
} else {
|
|
imageHeightAddition += 7.0
|
|
}
|
|
|
|
adjustedBoundingSize.height += imageHeightAddition + 5.0
|
|
}
|
|
|
|
if let (videoLayout, _) = contentInstantVideoSizeAndApply {
|
|
let imageHeightAddition = videoLayout.contentSize.height + 6.0
|
|
|
|
adjustedBoundingSize.height += imageHeightAddition// + 5.0
|
|
}
|
|
|
|
var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))?
|
|
if let continueActionButtonLayout = continueActionButtonLayout {
|
|
let (size, apply) = continueActionButtonLayout(boundingWidth - 5.0 - insets.right, 38.0)
|
|
actionButtonSizeAndApply = (size, apply)
|
|
adjustedBoundingSize.height += 4.0 + size.height
|
|
if let text, !text.isEmpty {
|
|
if contentImageSizeAndApply == nil {
|
|
adjustedBoundingSize.height += 5.0
|
|
} else if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
|
|
adjustedBoundingSize.height += 5.0
|
|
}
|
|
}
|
|
}
|
|
|
|
var statusSizeAndApply: ((CGSize), (ListViewItemUpdateAnimation) -> Void)?
|
|
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
|
|
statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth - insets.left - insets.right)
|
|
}
|
|
if let statusSizeAndApply = statusSizeAndApply {
|
|
adjustedBoundingSize.height += statusSizeAndApply.0.height
|
|
|
|
if let imageFrame = imageFrame, statusSizeAndApply.0.height == 0.0 {
|
|
if statusInText {
|
|
adjustedBoundingSize.height = max(adjustedBoundingSize.height, imageFrame.maxY + 8.0 + 15.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
adjustedBoundingSize.width = max(boundingWidth, adjustedBoundingSize.width)
|
|
|
|
var contentMediaHeight: CGFloat?
|
|
if let (contentImageSize, _) = contentImageSizeAndApply {
|
|
contentMediaHeight = contentImageSize.height
|
|
}
|
|
|
|
if let (contentFileSize, _) = contentFileSizeAndApply {
|
|
contentMediaHeight = contentFileSize.height
|
|
}
|
|
|
|
if let (videoLayout, _) = contentInstantVideoSizeAndApply {
|
|
contentMediaHeight = videoLayout.contentSize.height
|
|
}
|
|
|
|
var textVerticalOffset: CGFloat = 0.0
|
|
if titleBeforeMedia {
|
|
textVerticalOffset += topTitleLayout.size.height + 4.0
|
|
}
|
|
if let contentMediaHeight = contentMediaHeight, let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
|
|
textVerticalOffset += contentMediaHeight + 7.0
|
|
}
|
|
let adjustedTextFrame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset)
|
|
|
|
var statusFrame: CGRect?
|
|
if let statusSizeAndApply = statusSizeAndApply {
|
|
var finalStatusFrame = CGRect(origin: CGPoint(x: adjustedTextFrame.minX, y: adjustedTextFrame.maxY), size: statusSizeAndApply.0)
|
|
if let imageFrame = imageFrame {
|
|
if finalStatusFrame.maxY < imageFrame.maxY + 10.0 {
|
|
finalStatusFrame.origin.y = max(finalStatusFrame.minY, imageFrame.maxY + 2.0)
|
|
if finalStatusFrame.height == 0.0 {
|
|
finalStatusFrame.origin.y += 14.0
|
|
|
|
adjustedBoundingSize.height += 14.0
|
|
}
|
|
}
|
|
}
|
|
statusFrame = finalStatusFrame
|
|
}
|
|
|
|
return (adjustedBoundingSize, { [weak self] animation, synchronousLoads, applyInfo in
|
|
if let strongSelf = self {
|
|
strongSelf.context = context
|
|
strongSelf.message = message
|
|
strongSelf.media = mediaAndFlags?.0
|
|
strongSelf.theme = presentationData.theme
|
|
|
|
let backgroundView: UIImageView
|
|
if let current = strongSelf.backgroundView {
|
|
backgroundView = current
|
|
} else {
|
|
backgroundView = UIImageView()
|
|
strongSelf.backgroundView = backgroundView
|
|
strongSelf.view.insertSubview(backgroundView, at: 0)
|
|
}
|
|
|
|
if backgroundView.image == nil {
|
|
backgroundView.image = PresentationResourcesChat.chatReplyBackgroundTemplateImage(presentationData.theme.theme)
|
|
}
|
|
backgroundView.tintColor = mainColor
|
|
|
|
animation.animator.updateFrame(layer: backgroundView.layer, frame: CGRect(origin: CGPoint(x: 11.0, y: insets.top - 3.0), size: CGSize(width: adjustedBoundingSize.width - 4.0 - insets.right, height: adjustedBoundingSize.height - insets.top - insets.bottom + 4.0)), completion: nil)
|
|
backgroundView.isHidden = !displayLine
|
|
|
|
//strongSelf.borderColor = UIColor.red.cgColor
|
|
//strongSelf.borderWidth = 2.0
|
|
|
|
strongSelf.textNode.textNode.displaysAsynchronously = !isPreview
|
|
|
|
let _ = topTitleApply()
|
|
strongSelf.topTitleNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: insets.top), size: topTitleLayout.size)
|
|
|
|
let _ = textApply(TextNodeWithEntities.Arguments(
|
|
context: context,
|
|
cache: animationCache,
|
|
renderer: animationRenderer,
|
|
placeholderColor: messageTheme.mediaPlaceholderColor,
|
|
attemptSynchronous: synchronousLoads
|
|
))
|
|
switch strongSelf.visibility {
|
|
case .none:
|
|
strongSelf.textNode.visibilityRect = nil
|
|
case let .visible(_, subRect):
|
|
var subRect = subRect
|
|
subRect.origin.x = 0.0
|
|
subRect.size.width = 10000.0
|
|
strongSelf.textNode.visibilityRect = subRect
|
|
}
|
|
|
|
if let imageFrame = imageFrame {
|
|
if let updateImageSignal = updateInlineImageSignal {
|
|
strongSelf.inlineImageNode.setSignal(updateImageSignal)
|
|
}
|
|
animation.animator.updateFrame(layer: strongSelf.inlineImageNode.layer, frame: imageFrame, completion: nil)
|
|
if strongSelf.inlineImageNode.supernode == nil {
|
|
strongSelf.addSubnode(strongSelf.inlineImageNode)
|
|
}
|
|
|
|
if let imageApply = imageApply {
|
|
imageApply()
|
|
}
|
|
} else if strongSelf.inlineImageNode.supernode != nil {
|
|
strongSelf.inlineImageNode.removeFromSupernode()
|
|
}
|
|
|
|
if let (contentImageSize, contentImageApply) = contentImageSizeAndApply {
|
|
let contentImageNode = contentImageApply(animation, synchronousLoads)
|
|
if strongSelf.contentImageNode !== contentImageNode {
|
|
strongSelf.contentImageNode = contentImageNode
|
|
contentImageNode.activatePinch = { sourceNode in
|
|
controllerInteraction.activateMessagePinch(sourceNode)
|
|
}
|
|
strongSelf.addSubnode(contentImageNode)
|
|
contentImageNode.activateLocalContent = { [weak strongSelf] mode in
|
|
if let strongSelf = strongSelf {
|
|
strongSelf.openMedia?(mode)
|
|
}
|
|
}
|
|
contentImageNode.updateMessageReaction = { [weak controllerInteraction] message, value in
|
|
guard let controllerInteraction = controllerInteraction else {
|
|
return
|
|
}
|
|
controllerInteraction.updateMessageReaction(message, value)
|
|
}
|
|
contentImageNode.visibility = strongSelf.visibility != .none
|
|
}
|
|
let _ = contentImageApply(animation, synchronousLoads)
|
|
var contentImageFrame: CGRect
|
|
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
|
|
contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize)
|
|
if titleBeforeMedia {
|
|
contentImageFrame.origin.y += topTitleLayout.size.height + 4.0
|
|
}
|
|
} else {
|
|
contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentImageSize)
|
|
}
|
|
|
|
contentImageNode.frame = contentImageFrame
|
|
} else if let contentImageNode = strongSelf.contentImageNode {
|
|
contentImageNode.visibility = false
|
|
contentImageNode.removeFromSupernode()
|
|
strongSelf.contentImageNode = nil
|
|
}
|
|
|
|
if let updatedAdditionalImageBadge = updatedAdditionalImageBadge, let contentImageNode = strongSelf.contentImageNode, let contentImageSize = contentImageSizeAndApply?.0 {
|
|
if strongSelf.additionalImageBadgeNode != updatedAdditionalImageBadge {
|
|
strongSelf.additionalImageBadgeNode?.removeFromSupernode()
|
|
}
|
|
strongSelf.additionalImageBadgeNode = updatedAdditionalImageBadge
|
|
contentImageNode.addSubnode(updatedAdditionalImageBadge)
|
|
if mediaBadge != nil {
|
|
updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, animated: false)
|
|
updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 0.0, height: 0.0))
|
|
} else {
|
|
updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, alignment: .right, animated: false)
|
|
updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: contentImageSize.width - 6.0, y: contentImageSize.height - 18.0 - 6.0), size: CGSize(width: 0.0, height: 0.0))
|
|
}
|
|
} else if let additionalImageBadgeNode = strongSelf.additionalImageBadgeNode {
|
|
strongSelf.additionalImageBadgeNode = nil
|
|
additionalImageBadgeNode.removeFromSupernode()
|
|
}
|
|
|
|
if let (contentFileSize, contentFileApply) = contentFileSizeAndApply {
|
|
let contentFileNode = contentFileApply(synchronousLoads, animation, applyInfo)
|
|
if strongSelf.contentFileNode !== contentFileNode {
|
|
strongSelf.contentFileNode = contentFileNode
|
|
strongSelf.addSubnode(contentFileNode)
|
|
contentFileNode.activateLocalContent = { [weak strongSelf] in
|
|
if let strongSelf = strongSelf {
|
|
strongSelf.openMedia?(.default)
|
|
}
|
|
}
|
|
contentFileNode.requestUpdateLayout = { [weak strongSelf] _ in
|
|
if let strongSelf = strongSelf {
|
|
strongSelf.requestUpdateLayout?()
|
|
}
|
|
}
|
|
}
|
|
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
|
|
contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentFileSize)
|
|
} else {
|
|
contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 8.0 : 7.0)), size: contentFileSize)
|
|
}
|
|
} else if let contentFileNode = strongSelf.contentFileNode {
|
|
contentFileNode.removeFromSupernode()
|
|
strongSelf.contentFileNode = nil
|
|
}
|
|
|
|
if let (videoLayout, apply) = contentInstantVideoSizeAndApply {
|
|
let contentInstantVideoNode = apply(.unconstrained(width: boundingWidth - insets.left - insets.right), animation)
|
|
if strongSelf.contentInstantVideoNode !== contentInstantVideoNode {
|
|
strongSelf.contentInstantVideoNode = contentInstantVideoNode
|
|
strongSelf.addSubnode(contentInstantVideoNode)
|
|
}
|
|
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
|
|
contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: videoLayout.contentSize)
|
|
} else {
|
|
contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: videoLayout.contentSize)
|
|
}
|
|
} else if let contentInstantVideoNode = strongSelf.contentInstantVideoNode {
|
|
contentInstantVideoNode.removeFromSupernode()
|
|
strongSelf.contentInstantVideoNode = nil
|
|
}
|
|
|
|
strongSelf.textNode.textNode.frame = adjustedTextFrame
|
|
if let statusSizeAndApply = statusSizeAndApply, let statusFrame = statusFrame {
|
|
if strongSelf.statusNode.supernode == nil {
|
|
strongSelf.addSubnode(strongSelf.statusNode)
|
|
strongSelf.statusNode.frame = statusFrame
|
|
statusSizeAndApply.1(.None)
|
|
} else {
|
|
animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil)
|
|
statusSizeAndApply.1(animation)
|
|
}
|
|
} else if strongSelf.statusNode.supernode != nil {
|
|
strongSelf.statusNode.removeFromSupernode()
|
|
}
|
|
|
|
if let (size, apply) = actionButtonSizeAndApply {
|
|
let buttonNode = apply()
|
|
|
|
let buttonFrame = CGRect(origin: CGPoint(x: 12.0, y: adjustedBoundingSize.height - insets.bottom - size.height), size: size)
|
|
if buttonNode !== strongSelf.buttonNode {
|
|
strongSelf.buttonNode?.removeFromSupernode()
|
|
strongSelf.buttonNode = buttonNode
|
|
buttonNode.isUserInteractionEnabled = false
|
|
strongSelf.addSubnode(buttonNode)
|
|
buttonNode.pressed = {
|
|
if let strongSelf = self {
|
|
strongSelf.activateAction?()
|
|
}
|
|
}
|
|
buttonNode.frame = buttonFrame
|
|
} else {
|
|
animation.animator.updateFrame(layer: buttonNode.layer, frame: buttonFrame, completion: nil)
|
|
}
|
|
|
|
let buttonSeparatorFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + 8.0, y: buttonFrame.minY - 2.0), size: CGSize(width: buttonFrame.width - 8.0 - 8.0, height: UIScreenPixel))
|
|
|
|
let buttonSeparatorLayer: SimpleLayer
|
|
if let current = strongSelf.buttonSeparatorLayer {
|
|
buttonSeparatorLayer = current
|
|
animation.animator.updateFrame(layer: buttonSeparatorLayer, frame: buttonSeparatorFrame, completion: nil)
|
|
} else {
|
|
buttonSeparatorLayer = SimpleLayer()
|
|
strongSelf.buttonSeparatorLayer = buttonSeparatorLayer
|
|
strongSelf.layer.addSublayer(buttonSeparatorLayer)
|
|
buttonSeparatorLayer.frame = buttonSeparatorFrame
|
|
}
|
|
|
|
buttonSeparatorLayer.backgroundColor = mainColor.withMultipliedAlpha(0.5).cgColor
|
|
} else {
|
|
if let buttonNode = strongSelf.buttonNode {
|
|
strongSelf.buttonNode = nil
|
|
buttonNode.removeFromSupernode()
|
|
}
|
|
|
|
if let buttonSeparatorLayer = strongSelf.buttonSeparatorLayer {
|
|
strongSelf.buttonSeparatorLayer = nil
|
|
buttonSeparatorLayer.removeFromSuperlayer()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})*/
|
|
}
|
|
}
|
|
|
|
public func updateHiddenMedia(_ media: [Media]?) -> Bool {
|
|
if let currentMedia = self.media {
|
|
if let media = media {
|
|
var found = false
|
|
for m in media {
|
|
if currentMedia.isEqual(to: m) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if let contentImageNode = self.contentMedia {
|
|
contentImageNode.isHidden = found
|
|
contentImageNode.updateIsHidden(found)
|
|
return found
|
|
}
|
|
} else if let contentImageNode = self.contentMedia {
|
|
contentImageNode.isHidden = false
|
|
contentImageNode.updateIsHidden(false)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
public func transitionNode(media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
if let contentImageNode = self.contentMedia, let image = self.media as? TelegramMediaImage, image.isEqual(to: media) {
|
|
return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in
|
|
return (contentImageNode?.view.snapshotContentTree(unhide: true), nil)
|
|
})
|
|
} else if let contentImageNode = self.contentMedia, let file = self.media as? TelegramMediaFile, file.isEqual(to: media) {
|
|
return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in
|
|
return (contentImageNode?.view.snapshotContentTree(unhide: true), nil)
|
|
})
|
|
} else if let contentImageNode = self.contentMedia, let story = self.media as? TelegramMediaStory, story.isEqual(to: media) {
|
|
return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in
|
|
return (contentImageNode?.view.snapshotContentTree(unhide: true), nil)
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func hasActionAtPoint(_ point: CGPoint) -> Bool {
|
|
if let buttonNode = self.actionButton, buttonNode.frame.contains(point) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
|
if let text = self.text {
|
|
let textNodeFrame = text.textNode.frame
|
|
if let (index, attributes) = text.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
|
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
|
var concealed = true
|
|
if let (attributeText, fullText) = text.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
|
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
|
}
|
|
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
|
|
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
|
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
|
|
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
|
return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
|
|
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
|
|
return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
|
|
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
|
return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
|
|
}
|
|
}
|
|
}
|
|
|
|
if let actionButton = self.actionButton, actionButton.frame.contains(point) {
|
|
return ChatMessageBubbleContentTapAction(content: .ignore)
|
|
}
|
|
|
|
if let backgroundView = self.backgroundView, backgroundView.frame.contains(point) {
|
|
return self.defaultContentAction()
|
|
} else {
|
|
return .init(content: .none)
|
|
}
|
|
}
|
|
|
|
public func updateTouchesAtPoint(_ point: CGPoint?) {
|
|
guard let context = self.context, let message = self.message, let theme = self.theme else {
|
|
return
|
|
}
|
|
var rects: [CGRect]?
|
|
if let point = point {
|
|
if let text = self.text {
|
|
let textNodeFrame = text.textNode.frame
|
|
if let (index, attributes) = text.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
|
let possibleNames: [String] = [
|
|
TelegramTextAttributes.URL,
|
|
TelegramTextAttributes.PeerMention,
|
|
TelegramTextAttributes.PeerTextMention,
|
|
TelegramTextAttributes.BotCommand,
|
|
TelegramTextAttributes.Hashtag,
|
|
TelegramTextAttributes.BankCard
|
|
]
|
|
for name in possibleNames {
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
|
|
rects = text.textNode.attributeRects(name: name, at: index)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let rects = rects, let text = self.text {
|
|
let linkHighlightingNode: LinkHighlightingNode
|
|
if let current = self.linkHighlightingNode {
|
|
linkHighlightingNode = current
|
|
} else {
|
|
linkHighlightingNode = LinkHighlightingNode(color: message.effectivelyIncoming(context.account.peerId) ? theme.theme.chat.message.incoming.linkHighlightColor : theme.theme.chat.message.outgoing.linkHighlightColor)
|
|
self.linkHighlightingNode = linkHighlightingNode
|
|
self.transformContainer.insertSubnode(linkHighlightingNode, belowSubnode: text.textNode)
|
|
}
|
|
linkHighlightingNode.frame = text.textNode.frame
|
|
linkHighlightingNode.updateRects(rects)
|
|
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
|
self.linkHighlightingNode = nil
|
|
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
|
linkHighlightingNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
|
|
var isHighlighted = false
|
|
if rects == nil, let point {
|
|
if let actionButton = self.actionButton, actionButton.frame.contains(point) {
|
|
} else if let backgroundView = self.backgroundView, backgroundView.frame.contains(point) {
|
|
isHighlighted = true
|
|
}
|
|
}
|
|
|
|
if self.isHighlighted != isHighlighted {
|
|
self.isHighlighted = isHighlighted
|
|
|
|
if isHighlighted {
|
|
/*self.highlightTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false, block: { [weak self] timer in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.highlightTimer === timer {
|
|
self.highlightTimer = nil
|
|
}
|
|
self.applyIsHighlighted()
|
|
})*/
|
|
self.applyIsHighlighted()
|
|
} else {
|
|
self.applyIsHighlighted()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func applyIsHighlighted() {
|
|
if let highlightTimer = self.highlightTimer {
|
|
self.highlightTimer = nil
|
|
highlightTimer.invalidate()
|
|
}
|
|
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: self.isHighlighted ? 0.3 : 0.2, curve: .easeInOut)
|
|
let scale: CGFloat = self.isHighlighted ? ((self.bounds.width - 5.0) / self.bounds.width) : 1.0
|
|
transition.updateSublayerTransformScale(node: self.transformContainer, scale: scale, beginWithCurrentState: true)
|
|
}
|
|
|
|
public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
|
|
if let statusNode = self.statusNode, !statusNode.isHidden {
|
|
if let result = statusNode.reactionView(value: value) {
|
|
return result
|
|
}
|
|
}
|
|
if let result = self.contentFile?.dateAndStatusNode.reactionView(value: value) {
|
|
return result
|
|
}
|
|
if let result = self.contentMedia?.dateAndStatusNode.reactionView(value: value) {
|
|
return result
|
|
}
|
|
if let result = self.contentInstantVideo?.dateAndStatusNode.reactionView(value: value) {
|
|
return result
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
|
return self.contentMedia?.playMediaWithSound()
|
|
}
|
|
}
|