import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData import AccountContext import ContextUI import TelegramCore import TextFormat import ReactionSelectionNode import ViewControllerComponent import ComponentFlow import ComponentDisplayAdapters import ChatMessageBackground import WallpaperBackgroundNode import MultilineTextWithEntitiesComponent import ReactionButtonListComponent import MultilineTextComponent import ChatInputTextNode private final class EffectIcon: Component { enum Content: Equatable { case file(TelegramMediaFile) case text(String) } let context: AccountContext let content: Content init( context: AccountContext, content: Content ) { self.context = context self.content = content } static func ==(lhs: EffectIcon, rhs: EffectIcon) -> Bool { if lhs.context !== rhs.context { return false } if lhs.content != rhs.content { return false } return true } final class View: UIView { private var fileView: ReactionIconView? private var textView: ComponentView? override init(frame: CGRect) { super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: EffectIcon, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { if case let .file(file) = component.content { let fileView: ReactionIconView if let current = self.fileView { fileView = current } else { fileView = ReactionIconView() self.fileView = fileView self.addSubview(fileView) } fileView.update( size: availableSize, context: component.context, file: file, fileId: file.fileId.id, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, tintColor: nil, placeholderColor: UIColor(white: 0.0, alpha: 0.1), animateIdle: false, reaction: .custom(file.fileId.id), transition: .immediate ) fileView.frame = CGRect(origin: CGPoint(), size: availableSize) } else { if let fileView = self.fileView { self.fileView = nil fileView.removeFromSuperview() } } if case let .text(text) = component.content { let textView: ComponentView if let current = self.textView { textView = current } else { textView = ComponentView() self.textView = textView } let textSize = textView.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: text, font: Font.regular(10.0), textColor: .black)) )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - textSize.height) * 0.5)), size: textSize) if let textComponentView = textView.view { if textComponentView.superview == nil { self.addSubview(textComponentView) } textComponentView.frame = textFrame } } else { if let textView = self.textView { self.textView = nil textView.view?.removeFromSuperview() } } return availableSize } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } final class MessageItemView: UIView { private let backgroundWallpaperNode: ChatMessageBubbleBackdrop private let backgroundNode: ChatMessageBackground private let textClippingContainer: UIView private var textNode: ChatInputTextNode? private var effectIcon: ComponentView? var effectIconView: UIView? { return self.effectIcon?.view } private var chatTheme: ChatPresentationThemeData? private var currentSize: CGSize? override init(frame: CGRect) { self.backgroundWallpaperNode = ChatMessageBubbleBackdrop() self.backgroundNode = ChatMessageBackground() self.backgroundNode.backdropNode = self.backgroundWallpaperNode self.textClippingContainer = UIView() self.textClippingContainer.clipsToBounds = true super.init(frame: frame) self.addSubview(self.backgroundWallpaperNode.view) self.addSubview(self.backgroundNode.view) self.addSubview(self.textClippingContainer) } required init(coder: NSCoder) { preconditionFailure() } func update( context: AccountContext, presentationData: PresentationData, backgroundNode: WallpaperBackgroundNode?, textString: NSAttributedString, sourceTextInputView: ChatInputTextView?, textInsets: UIEdgeInsets, explicitBackgroundSize: CGSize?, maxTextWidth: CGFloat, maxTextHeight: CGFloat, effect: AvailableMessageEffects.MessageEffect?, transition: Transition ) -> CGSize { var effectIconSize: CGSize? if let effect { let effectIcon: ComponentView if let current = self.effectIcon { effectIcon = current } else { effectIcon = ComponentView() self.effectIcon = effectIcon } let effectIconContent: EffectIcon.Content if let staticIcon = effect.staticIcon { effectIconContent = .file(staticIcon) } else { effectIconContent = .text(effect.emoticon) } effectIconSize = effectIcon.update( transition: .immediate, component: AnyComponent(EffectIcon( context: context, content: effectIconContent )), environment: {}, containerSize: CGSize(width: 8.0, height: 8.0) ) } var textCutout: TextNodeCutout? if let effectIconSize { textCutout = TextNodeCutout(bottomRight: CGSize(width: effectIconSize.width + 4.0, height: effectIconSize.height)) } let _ = textCutout let textNode: ChatInputTextNode if let current = self.textNode { textNode = current } else { textNode = ChatInputTextNode(disableTiling: true) textNode.textView.isScrollEnabled = false textNode.isUserInteractionEnabled = false self.textNode = textNode self.textClippingContainer.addSubview(textNode.view) if let sourceTextInputView { textNode.textView.defaultTextContainerInset = sourceTextInputView.defaultTextContainerInset } let messageAttributedText = NSMutableAttributedString(attributedString: textString) messageAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: presentationData.theme.chat.message.outgoing.primaryTextColor, range: NSMakeRange(0, (messageAttributedText.string as NSString).length)) textNode.attributedText = messageAttributedText } let mainColor = presentationData.theme.chat.message.outgoing.accentControlColor let mappedLineStyle: ChatInputTextView.Theme.Quote.LineStyle if let sourceTextInputView, let textTheme = sourceTextInputView.theme { switch textTheme.quote.lineStyle { case .solid: mappedLineStyle = .solid(color: mainColor) case .doubleDashed: mappedLineStyle = .doubleDashed(mainColor: mainColor, secondaryColor: .clear) case .tripleDashed: mappedLineStyle = .tripleDashed(mainColor: mainColor, secondaryColor: .clear, tertiaryColor: .clear) } } else { mappedLineStyle = .solid(color: mainColor) } textNode.textView.theme = ChatInputTextView.Theme( quote: ChatInputTextView.Theme.Quote( background: mainColor.withMultipliedAlpha(0.1), foreground: mainColor, lineStyle: mappedLineStyle, codeBackground: mainColor.withMultipliedAlpha(0.1), codeForeground: mainColor ) ) let textPositioningInsets = UIEdgeInsets(top: -5.0, left: 0.0, bottom: -4.0, right: -4.0) var currentRightInset: CGFloat = 0.0 if let sourceTextInputView { currentRightInset = sourceTextInputView.currentRightInset } let textHeight = textNode.textHeightForWidth(maxTextWidth, rightInset: currentRightInset) textNode.updateLayout(size: CGSize(width: maxTextWidth, height: textHeight)) let textBoundingRect = textNode.textView.currentTextBoundingRect().integral let lastLineBoundingRect = textNode.textView.lastLineBoundingRect().integral let textWidth = textBoundingRect.width let textSize = CGSize(width: textWidth, height: textHeight) var positionedTextSize = CGSize(width: textSize.width + textPositioningInsets.left + textPositioningInsets.right, height: textSize.height + textPositioningInsets.top + textPositioningInsets.bottom) let effectInset: CGFloat = 12.0 if effect != nil, lastLineBoundingRect.width > textSize.width - effectInset { if lastLineBoundingRect != textBoundingRect { positionedTextSize.height += 11.0 } else { positionedTextSize.width += effectInset } } let unclippedPositionedTextHeight = positionedTextSize.height - (textPositioningInsets.top + textPositioningInsets.bottom) positionedTextSize.height = min(positionedTextSize.height, maxTextHeight) let size = CGSize(width: positionedTextSize.width + textInsets.left + textInsets.right, height: positionedTextSize.height + textInsets.top + textInsets.bottom) let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: positionedTextSize) let chatTheme: ChatPresentationThemeData if let current = self.chatTheme, current.theme === presentationData.theme { chatTheme = current } else { chatTheme = ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper) self.chatTheme = chatTheme } let themeGraphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper, bubbleCorners: presentationData.chatBubbleCorners) self.backgroundWallpaperNode.setType( type: .outgoing(.None), theme: chatTheme, essentialGraphics: themeGraphics, maskMode: true, backgroundNode: backgroundNode ) self.backgroundNode.setType( type: .outgoing(.None), highlighted: false, graphics: themeGraphics, maskMode: true, hasWallpaper: true, transition: transition.containedViewLayoutTransition, backgroundNode: backgroundNode ) let backgroundSize = explicitBackgroundSize ?? size let previousSize = self.currentSize self.currentSize = backgroundSize let textClippingContainerFrame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: backgroundSize.width - 1.0 - 7.0, height: backgroundSize.height - 1.0 - 1.0)) var textClippingContainerBounds = CGRect(origin: CGPoint(), size: textClippingContainerFrame.size) if explicitBackgroundSize != nil, let sourceTextInputView { textClippingContainerBounds.origin.y = sourceTextInputView.contentOffset.y } else { textClippingContainerBounds.origin.y = unclippedPositionedTextHeight - backgroundSize.height + 4.0 textClippingContainerBounds.origin.y = max(0.0, textClippingContainerBounds.origin.y) } transition.setPosition(view: self.textClippingContainer, position: textClippingContainerFrame.center) transition.setBounds(view: self.textClippingContainer, bounds: textClippingContainerBounds) textNode.view.frame = CGRect(origin: CGPoint(x: textFrame.minX + textPositioningInsets.left - textClippingContainerFrame.minX, y: textFrame.minY + textPositioningInsets.top - textClippingContainerFrame.minY), size: CGSize(width: maxTextWidth, height: textHeight)) if let effectIcon = self.effectIcon, let effectIconSize { if let effectIconView = effectIcon.view { var animateIn = false if effectIconView.superview == nil { animateIn = true self.addSubview(effectIconView) } let effectIconFrame = CGRect(origin: CGPoint(x: backgroundSize.width - textInsets.right + 2.0 - effectIconSize.width, y: backgroundSize.height - textInsets.bottom - 2.0 - effectIconSize.height), size: effectIconSize) if animateIn { if let previousSize { let previousEffectIconFrame = CGRect(origin: CGPoint(x: previousSize.width - textInsets.right + 2.0 - effectIconSize.width, y: previousSize.height - textInsets.bottom - 2.0 - effectIconSize.height), size: effectIconSize) effectIconView.frame = previousEffectIconFrame } else { effectIconView.frame = effectIconFrame } transition.animateAlpha(view: effectIconView, from: 0.0, to: 1.0) if !transition.animation.isImmediate { effectIconView.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) } } transition.setFrame(view: effectIconView, frame: effectIconFrame) } } else { if let effectIcon = self.effectIcon { self.effectIcon = nil if let effectIconView = effectIcon.view { let effectIconSize = effectIconView.bounds.size let effectIconFrame = CGRect(origin: CGPoint(x: backgroundSize.width - textInsets.right - effectIconSize.width, y: backgroundSize.height - textInsets.bottom - effectIconSize.height), size: effectIconSize) transition.setFrame(view: effectIconView, frame: effectIconFrame) transition.setScale(view: effectIconView, scale: 0.001) transition.setAlpha(view: effectIconView, alpha: 0.0, completion: { [weak effectIconView] _ in effectIconView?.removeFromSuperview() }) } } } let backgroundAlpha: CGFloat if explicitBackgroundSize != nil { backgroundAlpha = 0.0 } else { backgroundAlpha = 1.0 } transition.setFrame(view: self.backgroundWallpaperNode.view, frame: CGRect(origin: CGPoint(), size: backgroundSize)) transition.setAlpha(view: self.backgroundWallpaperNode.view, alpha: backgroundAlpha) self.backgroundWallpaperNode.updateFrame(CGRect(origin: CGPoint(), size: backgroundSize), transition: transition.containedViewLayoutTransition) transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: backgroundSize)) transition.setAlpha(view: self.backgroundNode.view, alpha: backgroundAlpha) self.backgroundNode.updateLayout(size: backgroundSize, transition: transition.containedViewLayoutTransition) return backgroundSize } }