mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
829 lines
40 KiB
Swift
829 lines
40 KiB
Swift
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
|
|
import EmojiTextAttachmentView
|
|
|
|
public final class ChatSendMessageScreenEffectIcon: Component {
|
|
public enum Content: Equatable {
|
|
case file(TelegramMediaFile)
|
|
case text(String)
|
|
}
|
|
|
|
public let context: AccountContext
|
|
public let content: Content
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
content: Content
|
|
) {
|
|
self.context = context
|
|
self.content = content
|
|
}
|
|
|
|
public static func ==(lhs: ChatSendMessageScreenEffectIcon, rhs: ChatSendMessageScreenEffectIcon) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.content != rhs.content {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public final class View: UIView {
|
|
private var fileView: ReactionIconView?
|
|
private var textView: ComponentView<Empty>?
|
|
|
|
override public init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: ChatSendMessageScreenEffectIcon, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> 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<Empty>
|
|
if let current = self.textView {
|
|
textView = current
|
|
} else {
|
|
textView = ComponentView()
|
|
self.textView = textView
|
|
}
|
|
let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)
|
|
let textSize = textView.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: text, font: Font.regular(10.0), textColor: .black)),
|
|
insets: textInsets
|
|
)),
|
|
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
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
final class CustomEmojiContainerView: UIView {
|
|
private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView?
|
|
|
|
private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:]
|
|
|
|
init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) {
|
|
self.emojiViewProvider = emojiViewProvider
|
|
|
|
super.init(frame: CGRect())
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
func update(emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) {
|
|
var nextIndexById: [Int64: Int] = [:]
|
|
|
|
var validKeys = Set<InlineStickerItemLayer.Key>()
|
|
for (rect, emoji) in emojiRects {
|
|
let index: Int
|
|
if let nextIndex = nextIndexById[emoji.fileId] {
|
|
index = nextIndex
|
|
} else {
|
|
index = 0
|
|
}
|
|
nextIndexById[emoji.fileId] = index + 1
|
|
|
|
let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index)
|
|
|
|
let view: UIView
|
|
if let current = self.emojiLayers[key] {
|
|
view = current
|
|
} else if let newView = self.emojiViewProvider(emoji) {
|
|
view = newView
|
|
self.addSubview(newView)
|
|
self.emojiLayers[key] = view
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
let size = CGSize(width: 24.0, height: 24.0)
|
|
|
|
view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0)), size: size)
|
|
|
|
validKeys.insert(key)
|
|
}
|
|
|
|
var removeKeys: [InlineStickerItemLayer.Key] = []
|
|
for (key, view) in self.emojiLayers {
|
|
if !validKeys.contains(key) {
|
|
removeKeys.append(key)
|
|
view.removeFromSuperview()
|
|
}
|
|
}
|
|
for key in removeKeys {
|
|
self.emojiLayers.removeValue(forKey: key)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
final class MessageItemView: UIView {
|
|
private let backgroundWallpaperNode: ChatMessageBubbleBackdrop
|
|
private let backgroundNode: ChatMessageBackground
|
|
|
|
private let textClippingContainer: UIView
|
|
private var textNode: ChatInputTextNode?
|
|
private var customEmojiContainerView: CustomEmojiContainerView?
|
|
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
|
|
|
private var mediaPreviewClippingView: UIView?
|
|
private var mediaPreview: ChatSendMessageContextScreenMediaPreview?
|
|
|
|
private var effectIcon: ComponentView<Empty>?
|
|
var effectIconView: UIView? {
|
|
return self.effectIcon?.view
|
|
}
|
|
|
|
private var effectIconBackgroundView: UIImageView?
|
|
|
|
private var chatTheme: ChatPresentationThemeData?
|
|
private var currentSize: CGSize?
|
|
private var currentMediaCaptionIsAbove: Bool = false
|
|
|
|
override init(frame: CGRect) {
|
|
self.backgroundWallpaperNode = ChatMessageBubbleBackdrop()
|
|
self.backgroundNode = ChatMessageBackground()
|
|
self.backgroundNode.backdropNode = self.backgroundWallpaperNode
|
|
|
|
self.textClippingContainer = UIView()
|
|
self.textClippingContainer.layer.anchorPoint = CGPoint()
|
|
self.textClippingContainer.clipsToBounds = true
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.isUserInteractionEnabled = false
|
|
|
|
self.addSubview(self.backgroundWallpaperNode.view)
|
|
self.addSubview(self.backgroundNode.view)
|
|
|
|
self.addSubview(self.textClippingContainer)
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
func animateIn(
|
|
sourceTextInputView: ChatInputTextView?,
|
|
isEditMessage: Bool,
|
|
transition: ComponentTransition
|
|
) {
|
|
if isEditMessage {
|
|
transition.animateScale(view: self, from: 0.001, to: 1.0)
|
|
transition.animateAlpha(view: self, from: 0.0, to: 1.0)
|
|
} else {
|
|
if let mediaPreview = self.mediaPreview {
|
|
mediaPreview.animateIn(transition: transition)
|
|
}
|
|
}
|
|
}
|
|
|
|
func animateOut(
|
|
sourceTextInputView: ChatInputTextView?,
|
|
toEmpty: Bool,
|
|
isEditMessage: Bool,
|
|
transition: ComponentTransition
|
|
) {
|
|
if isEditMessage {
|
|
transition.setScale(view: self, scale: 0.001)
|
|
transition.setAlpha(view: self, alpha: 0.0)
|
|
} else {
|
|
if let mediaPreview = self.mediaPreview {
|
|
if toEmpty {
|
|
mediaPreview.animateOutOnSend(transition: transition)
|
|
} else {
|
|
mediaPreview.animateOut(transition: transition)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func update(
|
|
context: AccountContext,
|
|
presentationData: PresentationData,
|
|
backgroundNode: WallpaperBackgroundNode?,
|
|
textString: NSAttributedString,
|
|
sourceTextInputView: ChatInputTextView?,
|
|
emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?,
|
|
sourceMediaPreview: ChatSendMessageContextScreenMediaPreview?,
|
|
mediaCaptionIsAbove: Bool,
|
|
textInsets: UIEdgeInsets,
|
|
explicitBackgroundSize: CGSize?,
|
|
maxTextWidth: CGFloat,
|
|
maxTextHeight: CGFloat,
|
|
containerSize: CGSize,
|
|
effect: AvailableMessageEffects.MessageEffect?,
|
|
isEditMessage: Bool,
|
|
transition: ComponentTransition
|
|
) -> CGSize {
|
|
self.emojiViewProvider = emojiViewProvider
|
|
|
|
var effectIconSize: CGSize?
|
|
if let effect {
|
|
let effectIcon: ComponentView<Empty>
|
|
if let current = self.effectIcon {
|
|
effectIcon = current
|
|
} else {
|
|
effectIcon = ComponentView()
|
|
self.effectIcon = effectIcon
|
|
}
|
|
let effectIconContent: ChatSendMessageScreenEffectIcon.Content
|
|
if let staticIcon = effect.staticIcon {
|
|
effectIconContent = .file(staticIcon)
|
|
} else {
|
|
effectIconContent = .text(effect.emoticon)
|
|
}
|
|
effectIconSize = effectIcon.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(ChatSendMessageScreenEffectIcon(
|
|
context: context,
|
|
content: effectIconContent
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 8.0, height: 8.0)
|
|
)
|
|
}
|
|
|
|
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 alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
|
|
|
|
if let sourceMediaPreview {
|
|
let mediaPreviewClippingView: UIView
|
|
if let current = self.mediaPreviewClippingView {
|
|
mediaPreviewClippingView = current
|
|
} else {
|
|
mediaPreviewClippingView = UIView()
|
|
mediaPreviewClippingView.layer.anchorPoint = CGPoint()
|
|
mediaPreviewClippingView.clipsToBounds = true
|
|
mediaPreviewClippingView.isUserInteractionEnabled = false
|
|
self.mediaPreviewClippingView = mediaPreviewClippingView
|
|
self.addSubview(mediaPreviewClippingView)
|
|
}
|
|
|
|
if self.mediaPreview !== sourceMediaPreview {
|
|
self.mediaPreview?.view.removeFromSuperview()
|
|
self.mediaPreview = nil
|
|
|
|
self.mediaPreview = sourceMediaPreview
|
|
if let mediaPreview = self.mediaPreview {
|
|
mediaPreviewClippingView.addSubview(mediaPreview.view)
|
|
}
|
|
}
|
|
|
|
let mediaPreviewSize = sourceMediaPreview.update(containerSize: containerSize, transition: transition)
|
|
|
|
var backgroundSize = CGSize(width: mediaPreviewSize.width, height: mediaPreviewSize.height)
|
|
var mediaPreviewFrame: CGRect
|
|
switch sourceMediaPreview.layoutType {
|
|
case .message, .media:
|
|
backgroundSize.width += 7.0
|
|
mediaPreviewFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: mediaPreviewSize)
|
|
case .videoMessage:
|
|
mediaPreviewFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: mediaPreviewSize)
|
|
}
|
|
|
|
let backgroundAlpha: CGFloat
|
|
switch sourceMediaPreview.layoutType {
|
|
case .media:
|
|
if textString.length != 0 {
|
|
backgroundAlpha = explicitBackgroundSize != nil ? 0.0 : 1.0
|
|
} else {
|
|
backgroundAlpha = 0.0
|
|
}
|
|
case .message, .videoMessage:
|
|
backgroundAlpha = 0.0
|
|
}
|
|
|
|
let backgroundScale: CGFloat = 1.0
|
|
|
|
var backgroundFrame = mediaPreviewFrame.insetBy(dx: -2.0, dy: -2.0)
|
|
backgroundFrame.size.width += 6.0
|
|
|
|
if textString.length != 0, case .media = sourceMediaPreview.layoutType {
|
|
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 {
|
|
var textContainerInset = sourceTextInputView.defaultTextContainerInset
|
|
textContainerInset.right = 0.0
|
|
textNode.textView.defaultTextContainerInset = textContainerInset
|
|
}
|
|
|
|
let messageAttributedText = NSMutableAttributedString(attributedString: textString)
|
|
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 maxTextWidth = mediaPreviewFrame.width
|
|
|
|
let textPositioningInsets = UIEdgeInsets(top: -5.0, left: 0.0, bottom: -4.0, right: -4.0)
|
|
|
|
let currentRightInset: CGFloat = 0.0
|
|
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)
|
|
|
|
var textFrame = CGRect(origin: CGPoint(x: textInsets.left - 6.0, y: backgroundFrame.height - 4.0 + textInsets.top), size: positionedTextSize)
|
|
if mediaCaptionIsAbove {
|
|
textFrame.origin.y = 5.0
|
|
}
|
|
|
|
backgroundFrame.size.height += textSize.height + 2.0
|
|
if mediaCaptionIsAbove {
|
|
mediaPreviewFrame.origin.y += textSize.height + 2.0
|
|
}
|
|
|
|
let backgroundSize = explicitBackgroundSize ?? size
|
|
|
|
let previousSize = self.currentSize
|
|
self.currentSize = backgroundFrame.size
|
|
let _ = previousSize
|
|
|
|
let textClippingContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + 1.0, y: backgroundFrame.minY + 1.0), size: CGSize(width: backgroundFrame.width - 1.0 - 7.0, height: backgroundFrame.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.origin)
|
|
transition.setBounds(view: self.textClippingContainer, bounds: textClippingContainerBounds)
|
|
|
|
alphaTransition.setAlpha(view: textNode.view, alpha: backgroundAlpha)
|
|
transition.setFrame(view: 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)))
|
|
self.updateTextContents()
|
|
}
|
|
|
|
transition.setFrame(view: sourceMediaPreview.view, frame: mediaPreviewFrame)
|
|
|
|
transition.setPosition(view: self.backgroundWallpaperNode.view, position: CGRect(origin: CGPoint(), size: backgroundFrame.size).center)
|
|
transition.setBounds(view: self.backgroundWallpaperNode.view, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
|
alphaTransition.setAlpha(view: self.backgroundWallpaperNode.view, alpha: backgroundAlpha)
|
|
transition.setScale(view: self.backgroundWallpaperNode.view, scale: backgroundScale)
|
|
self.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: transition.containedViewLayoutTransition)
|
|
transition.setPosition(view: self.backgroundNode.view, position: backgroundFrame.center)
|
|
transition.setBounds(view: self.backgroundNode.view, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
|
transition.setScale(view: self.backgroundNode.view, scale: backgroundScale)
|
|
alphaTransition.setAlpha(view: self.backgroundNode.view, alpha: backgroundAlpha)
|
|
self.backgroundNode.updateLayout(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition)
|
|
|
|
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 effectIconBackgroundView: UIImageView
|
|
if let current = self.effectIconBackgroundView {
|
|
effectIconBackgroundView = current
|
|
} else {
|
|
effectIconBackgroundView = UIImageView()
|
|
self.effectIconBackgroundView = effectIconBackgroundView
|
|
self.insertSubview(effectIconBackgroundView, belowSubview: effectIconView)
|
|
}
|
|
|
|
let effectIconBackgroundSize = CGSize(width: effectIconSize.width + 8.0 * 2.0, height: 18.0)
|
|
|
|
let effectIconBackgroundFrame: CGRect
|
|
switch sourceMediaPreview.layoutType {
|
|
case .message:
|
|
effectIconBackgroundFrame = CGRect(origin: CGPoint(x: mediaPreviewFrame.maxX - effectIconBackgroundSize.width - 3.0, y: mediaPreviewFrame.maxY - effectIconBackgroundSize.height - 4.0), size: effectIconBackgroundSize)
|
|
effectIconBackgroundView.backgroundColor = nil
|
|
case .media:
|
|
effectIconBackgroundFrame = CGRect(origin: CGPoint(x: mediaPreviewFrame.maxX - effectIconBackgroundSize.width - 6.0, y: mediaPreviewFrame.maxY - effectIconBackgroundSize.height - 6.0), size: effectIconBackgroundSize)
|
|
effectIconBackgroundView.backgroundColor = presentationData.theme.chat.message.mediaDateAndStatusFillColor
|
|
case .videoMessage:
|
|
effectIconBackgroundFrame = CGRect(origin: CGPoint(x: mediaPreviewFrame.maxX - effectIconBackgroundSize.width - 34.0, y: mediaPreviewFrame.maxY - effectIconBackgroundSize.height - 6.0), size: effectIconBackgroundSize)
|
|
|
|
let serviceMessageColors = serviceMessageColorComponents(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper)
|
|
|
|
effectIconBackgroundView.backgroundColor = serviceMessageColors.dateFillStatic
|
|
}
|
|
|
|
let effectIconFrame = CGRect(origin: CGPoint(x: effectIconBackgroundFrame.minX + floor((effectIconBackgroundFrame.width - effectIconSize.width) * 0.5), y: effectIconBackgroundFrame.minY + floor((effectIconBackgroundFrame.height - effectIconSize.height) * 0.5)), size: effectIconSize)
|
|
|
|
if animateIn {
|
|
effectIconView.frame = effectIconFrame
|
|
|
|
effectIconBackgroundView.frame = effectIconBackgroundFrame
|
|
effectIconBackgroundView.layer.cornerRadius = effectIconBackgroundFrame.height * 0.5
|
|
|
|
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.animateAlpha(view: effectIconBackgroundView, from: 0.0, to: 1.0)
|
|
}
|
|
|
|
transition.setFrame(view: effectIconView, frame: effectIconFrame)
|
|
|
|
transition.setFrame(view: effectIconBackgroundView, frame: effectIconBackgroundFrame)
|
|
transition.setCornerRadius(layer: effectIconBackgroundView.layer, cornerRadius: effectIconBackgroundFrame.height * 0.5)
|
|
}
|
|
} else {
|
|
if let effectIcon = self.effectIcon {
|
|
self.effectIcon = nil
|
|
|
|
if let effectIconView = effectIcon.view {
|
|
transition.setScale(view: effectIconView, scale: 0.001)
|
|
transition.setAlpha(view: effectIconView, alpha: 0.0, completion: { [weak effectIconView] _ in
|
|
effectIconView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
if let effectIconBackgroundView = self.effectIconBackgroundView {
|
|
self.effectIconBackgroundView = nil
|
|
transition.setAlpha(view: effectIconBackgroundView, alpha: 0.0, completion: { [weak effectIconBackgroundView] _ in
|
|
effectIconBackgroundView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
return backgroundFrame.size
|
|
} else {
|
|
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)
|
|
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 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.origin)
|
|
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))
|
|
self.updateTextContents()
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
func updateClippingRect(
|
|
sourceMediaPreview: ChatSendMessageContextScreenMediaPreview?,
|
|
isAnimatedIn: Bool,
|
|
localFrame: CGRect,
|
|
containerSize: CGSize,
|
|
transition: ComponentTransition
|
|
) {
|
|
if let mediaPreviewClippingView = self.mediaPreviewClippingView, let sourceMediaPreview {
|
|
let clippingFrame: CGRect
|
|
if !isAnimatedIn, let globalClippingRect = sourceMediaPreview.globalClippingRect {
|
|
clippingFrame = self.convert(globalClippingRect, from: nil)
|
|
} else {
|
|
clippingFrame = CGRect(origin: CGPoint(x: -localFrame.minX, y: -localFrame.minY), size: containerSize)
|
|
}
|
|
|
|
transition.setPosition(view: mediaPreviewClippingView, position: clippingFrame.origin)
|
|
transition.setBounds(view: mediaPreviewClippingView, bounds: CGRect(origin: CGPoint(x: clippingFrame.minX, y: clippingFrame.minY), size: clippingFrame.size))
|
|
}
|
|
}
|
|
|
|
private func updateTextContents() {
|
|
guard let textInputNode = self.textNode else {
|
|
return
|
|
}
|
|
|
|
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
|
|
|
|
if let attributedText = textInputNode.attributedText {
|
|
let beginning = textInputNode.textView.beginningOfDocument
|
|
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
|
|
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
|
if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) {
|
|
let textRects = textInputNode.textView.selectionRects(for: textRange)
|
|
for textRect in textRects {
|
|
customEmojiRects.append((textRect.rect, value))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
if !customEmojiRects.isEmpty {
|
|
let customEmojiContainerView: CustomEmojiContainerView
|
|
if let current = self.customEmojiContainerView {
|
|
customEmojiContainerView = current
|
|
} else {
|
|
customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
|
|
guard let self, let emojiViewProvider = self.emojiViewProvider else {
|
|
return nil
|
|
}
|
|
return emojiViewProvider(emoji)
|
|
})
|
|
customEmojiContainerView.isUserInteractionEnabled = false
|
|
textInputNode.textView.addSubview(customEmojiContainerView)
|
|
self.customEmojiContainerView = customEmojiContainerView
|
|
}
|
|
|
|
customEmojiContainerView.update(emojiRects: customEmojiRects)
|
|
} else {
|
|
if let customEmojiContainerView = self.customEmojiContainerView {
|
|
customEmojiContainerView.removeFromSuperview()
|
|
self.customEmojiContainerView = nil
|
|
}
|
|
}
|
|
}
|
|
}
|