mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
403 lines
20 KiB
Swift
403 lines
20 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import TelegramPresentationData
|
|
import AccountContext
|
|
import WallpaperBackgroundNode
|
|
|
|
public final class DrawingWallpaperRenderer {
|
|
private let context: AccountContext
|
|
private let dayWallpaper: TelegramWallpaper?
|
|
private let nightWallpaper: TelegramWallpaper?
|
|
|
|
private let wallpaperBackgroundNode: WallpaperBackgroundNode
|
|
private let darkWallpaperBackgroundNode: WallpaperBackgroundNode
|
|
|
|
public init (context: AccountContext, dayWallpaper: TelegramWallpaper?, nightWallpaper: TelegramWallpaper?) {
|
|
self.context = context
|
|
self.dayWallpaper = dayWallpaper
|
|
self.nightWallpaper = nightWallpaper
|
|
|
|
self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false)
|
|
self.wallpaperBackgroundNode.displaysAsynchronously = false
|
|
|
|
let wallpaper = self.dayWallpaper ?? context.sharedContext.currentPresentationData.with { $0 }.chatWallpaper
|
|
self.wallpaperBackgroundNode.update(wallpaper: wallpaper, animated: false)
|
|
|
|
self.darkWallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false)
|
|
self.darkWallpaperBackgroundNode.displaysAsynchronously = false
|
|
|
|
let darkTheme = defaultDarkColorPresentationTheme
|
|
let darkWallpaper = self.nightWallpaper ?? darkTheme.chat.defaultWallpaper
|
|
self.darkWallpaperBackgroundNode.update(wallpaper: darkWallpaper, animated: false)
|
|
}
|
|
|
|
public func render(completion: @escaping (CGSize, UIImage?, UIImage?, CGRect?) -> Void) {
|
|
self.updateLayout(size: CGSize(width: 360.0, height: 640.0))
|
|
|
|
let resultSize = CGSize(width: 1080, height: 1920)
|
|
Queue.mainQueue().justDispatch {
|
|
self.generate(view: self.wallpaperBackgroundNode.view) { dayImage in
|
|
if self.dayWallpaper != nil && self.nightWallpaper == nil {
|
|
completion(resultSize, dayImage, nil, nil)
|
|
} else {
|
|
Queue.mainQueue().justDispatch {
|
|
self.generate(view: self.darkWallpaperBackgroundNode.view) { nightImage in
|
|
completion(resultSize, dayImage, nightImage, nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func generate(view: UIView, completion: @escaping (UIImage) -> Void) {
|
|
let size = CGSize(width: 360.0, height: 640.0)
|
|
UIGraphicsBeginImageContextWithOptions(size, false, 3.0)
|
|
view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: true)
|
|
let img = UIGraphicsGetImageFromCurrentImageContext()
|
|
UIGraphicsEndImageContext()
|
|
|
|
let finalImage = generateImage(CGSize(width: size.width * 3.0, height: size.height * 3.0), contextGenerator: { size, context in
|
|
if let cgImage = img?.cgImage {
|
|
context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false)
|
|
}
|
|
}, opaque: true, scale: 1.0)
|
|
if let finalImage {
|
|
completion(finalImage)
|
|
}
|
|
}
|
|
|
|
private func updateLayout(size: CGSize) {
|
|
self.wallpaperBackgroundNode.updateLayout(size: size, displayMode: .aspectFill, transition: .immediate)
|
|
self.wallpaperBackgroundNode.frame = CGRect(origin: .zero, size: size)
|
|
self.darkWallpaperBackgroundNode.updateLayout(size: size, displayMode: .aspectFill, transition: .immediate)
|
|
self.darkWallpaperBackgroundNode.frame = CGRect(origin: .zero, size: size)
|
|
}
|
|
}
|
|
|
|
public final class DrawingMessageRenderer {
|
|
final class ContainerNode: ASDisplayNode {
|
|
private let context: AccountContext
|
|
private let messages: [Message]
|
|
private let isNight: Bool
|
|
private let isOverlay: Bool
|
|
private let isLink: Bool
|
|
|
|
private let messagesContainerNode: ASDisplayNode
|
|
private var avatarHeaderNode: ListViewItemHeaderNode?
|
|
private var messageNodes: [ListViewItemNode]?
|
|
|
|
init(context: AccountContext, messages: [Message], isNight: Bool = false, isOverlay: Bool = false, isLink: Bool = false) {
|
|
self.context = context
|
|
self.messages = messages
|
|
self.isNight = isNight
|
|
self.isOverlay = isOverlay
|
|
self.isLink = isLink
|
|
|
|
self.messagesContainerNode = ASDisplayNode()
|
|
self.messagesContainerNode.clipsToBounds = true
|
|
self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.messagesContainerNode)
|
|
}
|
|
|
|
public func render(presentationData: PresentationData, completion: @escaping (CGSize, UIImage?, CGRect?) -> Void) {
|
|
var mockPresentationData = presentationData
|
|
if self.isNight {
|
|
let darkTheme = defaultDarkColorPresentationTheme
|
|
mockPresentationData = mockPresentationData.withUpdated(theme: darkTheme).withUpdated(chatWallpaper: darkTheme.chat.defaultWallpaper)
|
|
}
|
|
|
|
let layout = ContainerViewLayout(size: CGSize(width: 360.0, height: 640.0), metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: .portrait), deviceMetrics: .iPhoneX, intrinsicInsets: .zero, safeInsets: .zero, additionalInsets: .zero, statusBarHeight: 0.0, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)
|
|
let size = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData)
|
|
let _ = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData)
|
|
|
|
Queue.mainQueue().after(0.2, {
|
|
var mediaRect: CGRect?
|
|
if let messageNode = self.messageNodes?.first {
|
|
if self.isOverlay {
|
|
func hideNonOverlayViews(_ view: UIView) -> Bool {
|
|
var hasResult = false
|
|
for view in view.subviews {
|
|
if view.tag == 0xFACE {
|
|
hasResult = true
|
|
} else {
|
|
if hideNonOverlayViews(view) {
|
|
hasResult = true
|
|
} else {
|
|
view.isHidden = true
|
|
}
|
|
}
|
|
}
|
|
return hasResult
|
|
}
|
|
let _ = hideNonOverlayViews(messageNode.view)
|
|
} else if !self.isNight {
|
|
func findMediaView(_ view: UIView) -> UIView? {
|
|
for view in view.subviews {
|
|
if let _ = view.asyncdisplaykit_node as? UniversalVideoNode {
|
|
return view
|
|
} else {
|
|
if let result = findMediaView(view) {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if let mediaView = findMediaView(messageNode.view) {
|
|
var rect = mediaView.convert(mediaView.bounds, to: self.messagesContainerNode.view)
|
|
rect.origin.y = self.messagesContainerNode.frame.height - rect.maxY
|
|
mediaRect = rect
|
|
}
|
|
}
|
|
}
|
|
|
|
self.generate(size: size) { image in
|
|
completion(size, image, mediaRect)
|
|
}
|
|
})
|
|
}
|
|
|
|
private func generate(size: CGSize, completion: @escaping (UIImage) -> Void) {
|
|
UIGraphicsBeginImageContextWithOptions(size, false, 3.0)
|
|
self.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: true)
|
|
let img = UIGraphicsGetImageFromCurrentImageContext()
|
|
UIGraphicsEndImageContext()
|
|
|
|
let finalImage = generateImage(CGSize(width: size.width * 3.0, height: size.height * 3.0), contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: .zero, size: size))
|
|
if let cgImage = img?.cgImage {
|
|
context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false)
|
|
}
|
|
}, opaque: false, scale: 1.0)
|
|
if let finalImage {
|
|
completion(finalImage)
|
|
}
|
|
}
|
|
|
|
private func updateMessagesLayout(layout: ContainerViewLayout, presentationData: PresentationData) -> CGSize {
|
|
let size = layout.size
|
|
|
|
let theme = presentationData.theme.withUpdated(preview: true)
|
|
|
|
let chatBubbleCorners = PresentationChatBubbleCorners(
|
|
mainRadius: presentationData.chatBubbleCorners.mainRadius,
|
|
auxiliaryRadius: presentationData.chatBubbleCorners.auxiliaryRadius,
|
|
mergeBubbleCorners: presentationData.chatBubbleCorners.mergeBubbleCorners,
|
|
hasTails: !self.isLink
|
|
)
|
|
|
|
let avatarHeaderItem: ListViewItemHeader?
|
|
if let author = self.messages.first?.author {
|
|
avatarHeaderItem = self.context.sharedContext.makeChatMessageAvatarHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, peer: self.messages.first!.peers[author.id]!, message: self.messages.first!, theme: theme, strings: presentationData.strings, wallpaper: presentationData.chatWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder)
|
|
} else {
|
|
avatarHeaderItem = nil
|
|
}
|
|
let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: presentationData.strings, wallpaper: presentationData.theme.chat.defaultWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)]
|
|
|
|
let inset: CGFloat = 16.0
|
|
var leftInset: CGFloat = 37.0
|
|
if self.isLink {
|
|
leftInset = -6.0
|
|
}
|
|
let containerWidth = layout.size.width - inset * 2.0
|
|
let params = ListViewItemLayoutParams(width: containerWidth, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height)
|
|
|
|
var width: CGFloat = containerWidth
|
|
var height: CGFloat = size.height
|
|
if let messageNodes = self.messageNodes {
|
|
for i in 0 ..< items.count {
|
|
let itemNode = messageNodes[i]
|
|
items[i].updateNode(async: { $0() }, node: {
|
|
return itemNode
|
|
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
|
|
let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: layout.size.height))
|
|
|
|
itemNode.contentSize = layout.contentSize
|
|
itemNode.insets = layout.insets
|
|
itemNode.frame = nodeFrame
|
|
itemNode.isUserInteractionEnabled = false
|
|
|
|
apply(ListViewItemApply(isOnScreen: true))
|
|
})
|
|
}
|
|
} else {
|
|
var messageNodes: [ListViewItemNode] = []
|
|
for i in 0 ..< items.count {
|
|
var itemNode: ListViewItemNode?
|
|
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: true, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
|
|
itemNode = node
|
|
apply().1(ListViewItemApply(isOnScreen: true))
|
|
})
|
|
itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
|
|
itemNode!.isUserInteractionEnabled = false
|
|
messageNodes.append(itemNode!)
|
|
self.messagesContainerNode.addSubnode(itemNode!)
|
|
}
|
|
self.messageNodes = messageNodes
|
|
}
|
|
|
|
if let messageNodes = self.messageNodes {
|
|
var minX: CGFloat = .greatestFiniteMagnitude
|
|
var maxX: CGFloat = -.greatestFiniteMagnitude
|
|
var minY: CGFloat = .greatestFiniteMagnitude
|
|
var maxY: CGFloat = -.greatestFiniteMagnitude
|
|
for node in messageNodes {
|
|
if node.frame.minY < minY {
|
|
minY = node.frame.minY
|
|
}
|
|
if node.frame.maxY > maxY {
|
|
maxY = node.frame.maxY
|
|
}
|
|
if let areaNode = node.subnodes?.last {
|
|
if areaNode.frame.minX < minX {
|
|
minX = areaNode.frame.minX
|
|
}
|
|
if areaNode.frame.maxX > maxX {
|
|
maxX = areaNode.frame.maxX
|
|
}
|
|
}
|
|
}
|
|
width = abs(maxX - minX)
|
|
height = abs(maxY - minY)
|
|
}
|
|
|
|
var bottomOffset: CGFloat = 0.0
|
|
if let messageNodes = self.messageNodes {
|
|
for itemNode in messageNodes {
|
|
itemNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: itemNode.frame.size)
|
|
bottomOffset += itemNode.frame.maxY
|
|
itemNode.updateFrame(itemNode.frame, within: layout.size)
|
|
}
|
|
}
|
|
|
|
if let avatarHeaderItem {
|
|
let avatarHeaderNode: ListViewItemHeaderNode
|
|
if let currentAvatarHeaderNode = self.avatarHeaderNode {
|
|
avatarHeaderNode = currentAvatarHeaderNode
|
|
avatarHeaderItem.updateNode(avatarHeaderNode, previous: nil, next: avatarHeaderItem)
|
|
} else {
|
|
avatarHeaderNode = avatarHeaderItem.node(synchronousLoad: true)
|
|
avatarHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
|
|
self.messagesContainerNode.addSubnode(avatarHeaderNode)
|
|
self.avatarHeaderNode = avatarHeaderNode
|
|
}
|
|
|
|
avatarHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: layout.size.width, height: avatarHeaderItem.height))
|
|
avatarHeaderNode.updateLayout(size: size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right)
|
|
}
|
|
|
|
var finalWidth: CGFloat = width
|
|
if leftInset > 0.0 {
|
|
finalWidth += leftInset + 6.0
|
|
}
|
|
|
|
let containerSize = CGSize(width: finalWidth, height: height)
|
|
self.frame = CGRect(origin: CGPoint(x: -1000.0, y: 0.0), size: containerSize)
|
|
self.messagesContainerNode.frame = CGRect(origin: CGPoint(), size: containerSize)
|
|
|
|
return containerSize
|
|
}
|
|
}
|
|
|
|
public struct Result {
|
|
public struct MediaFrame {
|
|
public let rect: CGRect
|
|
public let cornerRadius: CGFloat
|
|
}
|
|
|
|
public let size: CGSize
|
|
public let dayImage: UIImage
|
|
public let nightImage: UIImage
|
|
public let overlayImage: UIImage
|
|
public let mediaFrame: MediaFrame?
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private let messages: [Message]
|
|
|
|
private let dayContainerNode: ContainerNode
|
|
private let nightContainerNode: ContainerNode
|
|
private let overlayContainerNode: ContainerNode
|
|
|
|
public init(context: AccountContext, messages: [Message], parentView: UIView, isLink: Bool = false) {
|
|
self.context = context
|
|
self.messages = messages
|
|
|
|
self.dayContainerNode = ContainerNode(context: context, messages: messages, isLink: isLink)
|
|
self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true, isLink: isLink)
|
|
self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true, isLink: isLink)
|
|
|
|
parentView.addSubview(self.dayContainerNode.view)
|
|
parentView.addSubview(self.nightContainerNode.view)
|
|
parentView.addSubview(self.overlayContainerNode.view)
|
|
}
|
|
|
|
public func render(completion: @escaping (Result) -> Void) {
|
|
Queue.mainQueue().after(0.12) {
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
let defaultPresentationData = defaultPresentationData()
|
|
|
|
let mockPresentationData = PresentationData(
|
|
strings: presentationData.strings,
|
|
theme: defaultPresentationTheme,
|
|
autoNightModeTriggered: false,
|
|
chatWallpaper: presentationData.chatWallpaper,
|
|
chatFontSize: defaultPresentationData.chatFontSize,
|
|
chatBubbleCorners: defaultPresentationData.chatBubbleCorners,
|
|
listsFontSize: defaultPresentationData.listsFontSize,
|
|
dateTimeFormat: presentationData.dateTimeFormat,
|
|
nameDisplayOrder: presentationData.nameDisplayOrder,
|
|
nameSortOrder: presentationData.nameSortOrder,
|
|
reduceMotion: false,
|
|
largeEmoji: true
|
|
)
|
|
|
|
var finalSize: CGSize = .zero
|
|
var dayImage: UIImage?
|
|
var nightImage: UIImage?
|
|
var overlayImage: UIImage?
|
|
var mediaRect: CGRect?
|
|
|
|
let completeIfReady = {
|
|
if let dayImage, let nightImage, let overlayImage {
|
|
var cornerRadius: CGFloat = defaultPresentationData.chatBubbleCorners.mainRadius
|
|
if let mediaRect, mediaRect.width == mediaRect.height, mediaRect.width == 240.0 {
|
|
cornerRadius = mediaRect.width / 2.0
|
|
} else if let rect = mediaRect {
|
|
mediaRect = CGRect(x: rect.minX + 4.0, y: rect.minY, width: rect.width - 6.0, height: rect.height - 1.0)
|
|
}
|
|
completion(Result(size: finalSize, dayImage: dayImage, nightImage: nightImage, overlayImage: overlayImage, mediaFrame: mediaRect.flatMap { Result.MediaFrame(rect: $0, cornerRadius: cornerRadius) }))
|
|
|
|
self.dayContainerNode.view.removeFromSuperview()
|
|
self.nightContainerNode.view.removeFromSuperview()
|
|
self.overlayContainerNode.view.removeFromSuperview()
|
|
}
|
|
}
|
|
self.dayContainerNode.render(presentationData: mockPresentationData) { size, image, rect in
|
|
finalSize = size
|
|
dayImage = image
|
|
mediaRect = rect
|
|
completeIfReady()
|
|
}
|
|
self.nightContainerNode.render(presentationData: mockPresentationData) { size, image, _ in
|
|
nightImage = image
|
|
completeIfReady()
|
|
}
|
|
self.overlayContainerNode.render(presentationData: mockPresentationData) { size, image, _ in
|
|
overlayImage = image
|
|
completeIfReady()
|
|
}
|
|
}
|
|
}
|
|
}
|