mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[Temp] custom emoji layouts
This commit is contained in:
parent
2377d3e08c
commit
1ac654e8b2
@ -202,20 +202,26 @@ public struct Transition {
|
||||
}
|
||||
switch self.animation {
|
||||
case .none:
|
||||
view.bounds = CGRect(origin: view.bounds.origin, size: frame.size)
|
||||
view.layer.position = CGPoint(x: frame.midX, y: frame.midY)
|
||||
view.frame = frame
|
||||
//view.bounds = CGRect(origin: view.bounds.origin, size: frame.size)
|
||||
//view.layer.position = CGPoint(x: frame.midX, y: frame.midY)
|
||||
view.layer.removeAnimation(forKey: "position")
|
||||
view.layer.removeAnimation(forKey: "bounds")
|
||||
completion?(true)
|
||||
case .curve:
|
||||
let previousPosition = view.layer.presentation()?.position ?? view.center
|
||||
let previousBounds = view.layer.presentation()?.bounds ?? view.bounds
|
||||
let previousFrame: CGRect
|
||||
if (view.layer.animation(forKey: "position") != nil || view.layer.animation(forKey: "bounds") != nil), let presentation = view.layer.presentation() {
|
||||
previousFrame = presentation.frame
|
||||
} else {
|
||||
previousFrame = view.frame
|
||||
}
|
||||
|
||||
view.bounds = CGRect(origin: previousBounds.origin, size: frame.size)
|
||||
view.center = CGPoint(x: frame.midX, y: frame.midY)
|
||||
view.frame = frame
|
||||
//view.bounds = CGRect(origin: previousBounds.origin, size: frame.size)
|
||||
//view.center = CGPoint(x: frame.midX, y: frame.midY)
|
||||
|
||||
self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion)
|
||||
self.animateBounds(view: view, from: previousBounds, to: view.bounds)
|
||||
self.animatePosition(view: view, from: CGPoint(x: previousFrame.midX, y: previousFrame.midY), to: CGPoint(x: frame.midX, y: frame.midY), completion: completion)
|
||||
self.animateBounds(view: view, from: CGRect(origin: view.bounds.origin, size: previousFrame.size), to: CGRect(origin: view.bounds.origin, size: frame.size))
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,7 +236,12 @@ public struct Transition {
|
||||
view.layer.removeAnimation(forKey: "bounds")
|
||||
completion?(true)
|
||||
case .curve:
|
||||
let previousBounds = view.layer.presentation()?.bounds ?? view.bounds
|
||||
let previousBounds: CGRect
|
||||
if view.layer.animation(forKey: "bounds") != nil, let presentation = view.layer.presentation() {
|
||||
previousBounds = presentation.bounds
|
||||
} else {
|
||||
previousBounds = view.layer.bounds
|
||||
}
|
||||
view.bounds = bounds
|
||||
|
||||
self.animateBounds(view: view, from: previousBounds, to: view.bounds, completion: completion)
|
||||
@ -248,7 +259,12 @@ public struct Transition {
|
||||
view.layer.removeAnimation(forKey: "position")
|
||||
completion?(true)
|
||||
case .curve:
|
||||
let previousPosition = view.layer.presentation()?.position ?? view.center
|
||||
let previousPosition: CGPoint
|
||||
if view.layer.animation(forKey: "position") != nil, let presentation = view.layer.presentation() {
|
||||
previousPosition = presentation.position
|
||||
} else {
|
||||
previousPosition = view.layer.position
|
||||
}
|
||||
view.center = position
|
||||
|
||||
self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion)
|
||||
@ -266,7 +282,12 @@ public struct Transition {
|
||||
layer.removeAnimation(forKey: "bounds")
|
||||
completion?(true)
|
||||
case .curve:
|
||||
let previousBounds = layer.presentation()?.bounds ?? layer.bounds
|
||||
let previousBounds: CGRect
|
||||
if layer.animation(forKey: "bounds") != nil, let presentation = layer.presentation() {
|
||||
previousBounds = presentation.bounds
|
||||
} else {
|
||||
previousBounds = layer.bounds
|
||||
}
|
||||
layer.bounds = bounds
|
||||
|
||||
self.animateBounds(layer: layer, from: previousBounds, to: layer.bounds, completion: completion)
|
||||
@ -284,7 +305,12 @@ public struct Transition {
|
||||
layer.removeAnimation(forKey: "position")
|
||||
completion?(true)
|
||||
case .curve:
|
||||
let previousPosition = layer.presentation()?.position ?? layer.position
|
||||
let previousPosition: CGPoint
|
||||
if layer.animation(forKey: "position") != nil, let presentation = layer.presentation() {
|
||||
previousPosition = presentation.position
|
||||
} else {
|
||||
previousPosition = layer.position
|
||||
}
|
||||
layer.position = position
|
||||
|
||||
self.animatePosition(layer: layer, from: previousPosition, to: layer.position, completion: completion)
|
||||
|
@ -5,6 +5,12 @@ public struct ImmediateTextNodeLayoutInfo {
|
||||
public let size: CGSize
|
||||
public let truncated: Bool
|
||||
public let numberOfLines: Int
|
||||
|
||||
public init(size: CGSize, truncated: Bool, numberOfLines: Int) {
|
||||
self.size = size
|
||||
self.truncated = truncated
|
||||
self.numberOfLines = numberOfLines
|
||||
}
|
||||
}
|
||||
|
||||
public class ImmediateTextNode: TextNode {
|
||||
@ -125,7 +131,7 @@ public class ImmediateTextNode: TextNode {
|
||||
}
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
override open func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.updateInteractiveActions()
|
||||
|
@ -148,7 +148,23 @@ public final class TextNodeLayoutArguments {
|
||||
public let textStroke: (UIColor, CGFloat)?
|
||||
public let displaySpoilers: Bool
|
||||
|
||||
public init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, minimumNumberOfLines: Int = 0, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, verticalAlignment: TextVerticalAlignment = .top, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), lineColor: UIColor? = nil, textShadowColor: UIColor? = nil, textStroke: (UIColor, CGFloat)? = nil, displaySpoilers: Bool = false) {
|
||||
public init(
|
||||
attributedString: NSAttributedString?,
|
||||
backgroundColor: UIColor? = nil,
|
||||
minimumNumberOfLines: Int = 0,
|
||||
maximumNumberOfLines: Int,
|
||||
truncationType: CTLineTruncationType,
|
||||
constrainedSize: CGSize,
|
||||
alignment: NSTextAlignment = .natural,
|
||||
verticalAlignment: TextVerticalAlignment = .top,
|
||||
lineSpacing: CGFloat = 0.12,
|
||||
cutout: TextNodeCutout? = nil,
|
||||
insets: UIEdgeInsets = UIEdgeInsets(),
|
||||
lineColor: UIColor? = nil,
|
||||
textShadowColor: UIColor? = nil,
|
||||
textStroke: (UIColor, CGFloat)? = nil,
|
||||
displaySpoilers: Bool = false
|
||||
) {
|
||||
self.attributedString = attributedString
|
||||
self.backgroundColor = backgroundColor
|
||||
self.minimumNumberOfLines = minimumNumberOfLines
|
||||
@ -165,6 +181,26 @@ public final class TextNodeLayoutArguments {
|
||||
self.textStroke = textStroke
|
||||
self.displaySpoilers = displaySpoilers
|
||||
}
|
||||
|
||||
public func withAttributedString(_ attributedString: NSAttributedString?) -> TextNodeLayoutArguments {
|
||||
return TextNodeLayoutArguments(
|
||||
attributedString: attributedString,
|
||||
backgroundColor: self.backgroundColor,
|
||||
minimumNumberOfLines: self.minimumNumberOfLines,
|
||||
maximumNumberOfLines: self.maximumNumberOfLines,
|
||||
truncationType: self.truncationType,
|
||||
constrainedSize: self.constrainedSize,
|
||||
alignment: self.alignment,
|
||||
verticalAlignment: self.verticalAlignment,
|
||||
lineSpacing: self.lineSpacing,
|
||||
cutout: self.cutout,
|
||||
insets: self.insets,
|
||||
lineColor: self.lineColor,
|
||||
textShadowColor: self.textShadowColor,
|
||||
textStroke: self.textStroke,
|
||||
displaySpoilers: self.displaySpoilers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public final class TextNodeLayout: NSObject {
|
||||
@ -881,7 +917,7 @@ public final class TextAccessibilityOverlayNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public class TextNode: ASDisplayNode {
|
||||
open class TextNode: ASDisplayNode {
|
||||
public internal(set) var cachedLayout: TextNodeLayout?
|
||||
|
||||
override public init() {
|
||||
@ -892,7 +928,7 @@ public class TextNode: ASDisplayNode {
|
||||
self.clipsToBounds = false
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
override open func didLoad() {
|
||||
super.didLoad()
|
||||
}
|
||||
|
||||
|
@ -242,7 +242,7 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
||||
|
||||
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: .firstLast, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId)
|
||||
var text = !item.message.text.isEmpty ? item.message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0
|
||||
var text = !item.message.text.isEmpty ? item.message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0.string
|
||||
text = foldLineBreaks(text)
|
||||
|
||||
var contentImageMedia: Media?
|
||||
|
@ -3,6 +3,7 @@ import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import PlatformRestrictionMatching
|
||||
import TextFormat
|
||||
|
||||
public enum MessageContentKindKey {
|
||||
case text
|
||||
@ -26,7 +27,7 @@ public enum MessageContentKindKey {
|
||||
}
|
||||
|
||||
public enum MessageContentKind: Equatable {
|
||||
case text(String)
|
||||
case text(NSAttributedString)
|
||||
case image
|
||||
case video
|
||||
case videoMessage
|
||||
@ -87,6 +88,40 @@ public enum MessageContentKind: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public func messageTextWithAttributes(message: EngineMessage) -> NSAttributedString {
|
||||
var attributedText = NSAttributedString(string: message.text)
|
||||
|
||||
var entities: TextEntitiesMessageAttribute?
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
entities = attribute
|
||||
break
|
||||
}
|
||||
}
|
||||
if let entities = entities?.entities {
|
||||
let updatedString = NSMutableAttributedString(attributedString: attributedText)
|
||||
|
||||
for entity in entities.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) {
|
||||
guard case let .CustomEmoji(stickerPack, fileId) = entity.type else {
|
||||
continue
|
||||
}
|
||||
|
||||
let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||
|
||||
let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil)
|
||||
var updatedAttributes: [NSAttributedString.Key: Any] = currentDict
|
||||
//updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor
|
||||
updatedAttributes[ChatTextInputAttributes.customEmoji] = ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId)
|
||||
|
||||
let insertString = NSAttributedString(string: updatedString.attributedSubstring(from: range).string, attributes: updatedAttributes)
|
||||
updatedString.replaceCharacters(in: range, with: insertString)
|
||||
}
|
||||
attributedText = updatedString
|
||||
}
|
||||
|
||||
return attributedText
|
||||
}
|
||||
|
||||
public func messageContentKind(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> MessageContentKind {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute {
|
||||
@ -101,7 +136,7 @@ public func messageContentKind(contentSettings: ContentSettings, message: Engine
|
||||
return kind
|
||||
}
|
||||
}
|
||||
return .text(message.text)
|
||||
return .text(messageTextWithAttributes(message: message))
|
||||
}
|
||||
|
||||
public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, dateTimeFormat: PresentationDateTimeFormat? = nil, accountPeerId: EnginePeer.Id? = nil) -> MessageContentKind? {
|
||||
@ -170,7 +205,7 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil
|
||||
}
|
||||
case .action:
|
||||
if let message = message, let strings = strings, let nameDisplayOrder = nameDisplayOrder, let accountPeerId = accountPeerId {
|
||||
return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat ?? PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), message: message, accountPeerId: accountPeerId, forChatList: false)?.0 ?? "")
|
||||
return .text(NSAttributedString(string: plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat ?? PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), message: message, accountPeerId: accountPeerId, forChatList: false)?.0 ?? ""))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -189,59 +224,59 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func stringForMediaKind(_ kind: MessageContentKind, strings: PresentationStrings) -> (String, Bool) {
|
||||
public func stringForMediaKind(_ kind: MessageContentKind, strings: PresentationStrings) -> (NSAttributedString, Bool) {
|
||||
switch kind {
|
||||
case let .text(text):
|
||||
return (foldLineBreaks(text), false)
|
||||
case .image:
|
||||
return (strings.Message_Photo, true)
|
||||
return (NSAttributedString(string: strings.Message_Photo), true)
|
||||
case .video:
|
||||
return (strings.Message_Video, true)
|
||||
return (NSAttributedString(string: strings.Message_Video), true)
|
||||
case .videoMessage:
|
||||
return (strings.Message_VideoMessage, true)
|
||||
return (NSAttributedString(string: strings.Message_VideoMessage), true)
|
||||
case .audioMessage:
|
||||
return (strings.Message_Audio, true)
|
||||
return (NSAttributedString(string: strings.Message_Audio), true)
|
||||
case let .sticker(text):
|
||||
if text.isEmpty {
|
||||
return (strings.Message_Sticker, true)
|
||||
return (NSAttributedString(string: strings.Message_Sticker), true)
|
||||
} else {
|
||||
return (strings.Message_StickerText(text).string, true)
|
||||
return (NSAttributedString(string: strings.Message_StickerText(text).string), true)
|
||||
}
|
||||
case .animation:
|
||||
return (strings.Message_Animation, true)
|
||||
return (NSAttributedString(string: strings.Message_Animation), true)
|
||||
case let .file(text):
|
||||
if text.isEmpty {
|
||||
return (strings.Message_File, true)
|
||||
return (NSAttributedString(string: strings.Message_File), true)
|
||||
} else {
|
||||
return (text, true)
|
||||
return (NSAttributedString(string: text), true)
|
||||
}
|
||||
case .contact:
|
||||
return (strings.Message_Contact, true)
|
||||
return (NSAttributedString(string: strings.Message_Contact), true)
|
||||
case let .game(text):
|
||||
return (text, true)
|
||||
return (NSAttributedString(string: text), true)
|
||||
case .location:
|
||||
return (strings.Message_Location, true)
|
||||
return (NSAttributedString(string: strings.Message_Location), true)
|
||||
case .liveLocation:
|
||||
return (strings.Message_LiveLocation, true)
|
||||
return (NSAttributedString(string: strings.Message_LiveLocation), true)
|
||||
case .expiredImage:
|
||||
return (strings.Message_ImageExpired, true)
|
||||
return (NSAttributedString(string: strings.Message_ImageExpired), true)
|
||||
case .expiredVideo:
|
||||
return (strings.Message_VideoExpired, true)
|
||||
return (NSAttributedString(string: strings.Message_VideoExpired), true)
|
||||
case let .poll(text):
|
||||
return ("📊 \(text)", false)
|
||||
return (NSAttributedString(string: "📊 \(text)"), false)
|
||||
case let .restricted(text):
|
||||
return (text, false)
|
||||
return (NSAttributedString(string: text), false)
|
||||
case let .dice(emoji):
|
||||
return (emoji, true)
|
||||
return (NSAttributedString(string: emoji), true)
|
||||
case let .invoice(text):
|
||||
return (text, true)
|
||||
return (NSAttributedString(string: text), true)
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionStringForMessage(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> (String, Bool, Bool) {
|
||||
public func descriptionStringForMessage(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> (NSAttributedString, Bool, Bool) {
|
||||
let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId)
|
||||
if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) {
|
||||
return (foldLineBreaks(message.text), false, true)
|
||||
return (foldLineBreaks(messageTextWithAttributes(message: message)), false, true)
|
||||
}
|
||||
let result = stringForMediaKind(contentKind, strings: strings)
|
||||
return (result.0, result.1, false)
|
||||
@ -263,6 +298,10 @@ public func foldLineBreaks(_ text: String) -> String {
|
||||
return result
|
||||
}
|
||||
|
||||
public func foldLineBreaks(_ text: NSAttributedString) -> NSAttributedString {
|
||||
//TODO:localize
|
||||
return text
|
||||
}
|
||||
|
||||
public func trimToLineCount(_ text: String, lineCount: Int) -> String {
|
||||
if lineCount < 1 {
|
||||
|
@ -288,6 +288,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/ChatInputPanelContainer:ChatInputPanelContainer",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
|
||||
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||
"//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC",
|
||||
"//submodules/Media/LocalAudioTranscription:LocalAudioTranscription",
|
||||
|
@ -54,7 +54,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
}
|
||||
}
|
||||
|
||||
public init(context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) {
|
||||
public init(context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) {
|
||||
self.context = context
|
||||
self.groupId = groupId
|
||||
self.emoji = emoji
|
||||
@ -63,7 +63,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
self.placeholderColor = placeholderColor
|
||||
|
||||
let scale = min(2.0, UIScreenScale)
|
||||
self.pointSize = CGSize(width: 24, height: 24)
|
||||
self.pointSize = pointSize
|
||||
self.pixelSize = CGSize(width: self.pointSize.width * scale, height: self.pointSize.height * scale)
|
||||
|
||||
super.init()
|
||||
@ -180,7 +180,7 @@ public final class EmojiTextAttachmentView: UIView {
|
||||
private let contentLayer: InlineStickerItemLayer
|
||||
|
||||
public init(context: AccountContext, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) {
|
||||
self.contentLayer = InlineStickerItemLayer(context: context, groupId: "textInputView", attemptSynchronousLoad: true, emoji: emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor)
|
||||
self.contentLayer = InlineStickerItemLayer(context: context, groupId: "textInputView", attemptSynchronousLoad: true, emoji: emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: 24.0, height: 24.0))
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
|
@ -41,16 +41,43 @@ public final class EntityKeyboardComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public struct GifSearchEmoji: Equatable {
|
||||
public var emoji: String
|
||||
public var file: TelegramMediaFile
|
||||
public var title: String
|
||||
|
||||
public init(emoji: String, file: TelegramMediaFile, title: String) {
|
||||
self.emoji = emoji
|
||||
self.file = file
|
||||
self.title = title
|
||||
}
|
||||
|
||||
public static func ==(lhs: GifSearchEmoji, rhs: GifSearchEmoji) -> Bool {
|
||||
if lhs.emoji != rhs.emoji {
|
||||
return false
|
||||
}
|
||||
if lhs.file.fileId != rhs.file.fileId {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public let theme: PresentationTheme
|
||||
public let bottomInset: CGFloat
|
||||
public let emojiContent: EmojiPagerContentComponent
|
||||
public let stickerContent: EmojiPagerContentComponent
|
||||
public let gifContent: GifPagerContentComponent
|
||||
public let availableGifSearchEmojies: [GifSearchEmoji]
|
||||
public let defaultToEmojiTab: Bool
|
||||
public let externalTopPanelContainer: PagerExternalTopPanelContainer?
|
||||
public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void
|
||||
public let hideInputUpdated: (Bool, Bool, Transition) -> Void
|
||||
public let switchToTextInput: () -> Void
|
||||
public let switchToGifSubject: (GifPagerContentComponent.Subject) -> Void
|
||||
public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode
|
||||
public let deviceMetrics: DeviceMetrics
|
||||
public let hiddenInputHeight: CGFloat
|
||||
@ -62,11 +89,13 @@ public final class EntityKeyboardComponent: Component {
|
||||
emojiContent: EmojiPagerContentComponent,
|
||||
stickerContent: EmojiPagerContentComponent,
|
||||
gifContent: GifPagerContentComponent,
|
||||
availableGifSearchEmojies: [GifSearchEmoji],
|
||||
defaultToEmojiTab: Bool,
|
||||
externalTopPanelContainer: PagerExternalTopPanelContainer?,
|
||||
topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void,
|
||||
hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void,
|
||||
switchToTextInput: @escaping () -> Void,
|
||||
switchToGifSubject: @escaping (GifPagerContentComponent.Subject) -> Void,
|
||||
makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode,
|
||||
deviceMetrics: DeviceMetrics,
|
||||
hiddenInputHeight: CGFloat,
|
||||
@ -77,11 +106,13 @@ public final class EntityKeyboardComponent: Component {
|
||||
self.emojiContent = emojiContent
|
||||
self.stickerContent = stickerContent
|
||||
self.gifContent = gifContent
|
||||
self.availableGifSearchEmojies = availableGifSearchEmojies
|
||||
self.defaultToEmojiTab = defaultToEmojiTab
|
||||
self.externalTopPanelContainer = externalTopPanelContainer
|
||||
self.topPanelExtensionUpdated = topPanelExtensionUpdated
|
||||
self.hideInputUpdated = hideInputUpdated
|
||||
self.switchToTextInput = switchToTextInput
|
||||
self.switchToGifSubject = switchToGifSubject
|
||||
self.makeSearchContainerNode = makeSearchContainerNode
|
||||
self.deviceMetrics = deviceMetrics
|
||||
self.hiddenInputHeight = hiddenInputHeight
|
||||
@ -104,6 +135,9 @@ public final class EntityKeyboardComponent: Component {
|
||||
if lhs.gifContent != rhs.gifContent {
|
||||
return false
|
||||
}
|
||||
if lhs.availableGifSearchEmojies != rhs.availableGifSearchEmojies {
|
||||
return false
|
||||
}
|
||||
if lhs.defaultToEmojiTab != rhs.defaultToEmojiTab {
|
||||
return false
|
||||
}
|
||||
@ -162,26 +196,58 @@ public final class EntityKeyboardComponent: Component {
|
||||
let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
|
||||
contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(component.gifContent)))
|
||||
var topGifItems: [EntityKeyboardTopPanelComponent.Item] = []
|
||||
topGifItems.removeAll()
|
||||
/*topGifItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||
//TODO:localize
|
||||
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||
id: "recent",
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/RecentTabIcon",
|
||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
maxSize: CGSize(width: 30.0, height: 30.0))
|
||||
)
|
||||
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
|
||||
imageName: "Chat/Input/Media/RecentTabIcon",
|
||||
theme: component.theme,
|
||||
title: "Recent",
|
||||
pressed: { [weak self] in
|
||||
self?.component?.switchToGifSubject(.recent)
|
||||
}
|
||||
))
|
||||
))
|
||||
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||
id: "trending",
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/TrendingGifs",
|
||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
maxSize: CGSize(width: 30.0, height: 30.0))
|
||||
)
|
||||
))*/
|
||||
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
|
||||
imageName: "Chat/Input/Media/TrendingGifs",
|
||||
theme: component.theme,
|
||||
title: "Trending",
|
||||
pressed: { [weak self] in
|
||||
self?.component?.switchToGifSubject(.trending)
|
||||
}
|
||||
))
|
||||
))
|
||||
for emoji in component.availableGifSearchEmojies {
|
||||
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||
id: emoji.emoji,
|
||||
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
|
||||
context: component.stickerContent.context,
|
||||
file: emoji.file,
|
||||
animationCache: component.stickerContent.animationCache,
|
||||
animationRenderer: component.stickerContent.animationRenderer,
|
||||
theme: component.theme,
|
||||
title: emoji.title,
|
||||
pressed: { [weak self] in
|
||||
self?.component?.switchToGifSubject(.emojiSearch(emoji.emoji))
|
||||
}
|
||||
))
|
||||
))
|
||||
}
|
||||
let defaultActiveGifItemId: AnyHashable
|
||||
switch component.gifContent.subject {
|
||||
case .recent:
|
||||
defaultActiveGifItemId = "recent"
|
||||
case .trending:
|
||||
defaultActiveGifItemId = "trending"
|
||||
case let .emojiSearch(value):
|
||||
defaultActiveGifItemId = AnyHashable(value)
|
||||
}
|
||||
contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent(
|
||||
theme: component.theme,
|
||||
items: topGifItems,
|
||||
defaultActiveItemId: defaultActiveGifItemId,
|
||||
activeContentItemIdUpdated: gifsContentItemIdUpdated
|
||||
))))
|
||||
contentIcons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(BundleIconComponent(
|
||||
|
@ -364,15 +364,18 @@ final class EntityKeyboardTopPanelComponent: Component {
|
||||
|
||||
let theme: PresentationTheme
|
||||
let items: [Item]
|
||||
let defaultActiveItemId: AnyHashable?
|
||||
let activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)>
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
items: [Item],
|
||||
defaultActiveItemId: AnyHashable? = nil,
|
||||
activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)>
|
||||
) {
|
||||
self.theme = theme
|
||||
self.items = items
|
||||
self.defaultActiveItemId = defaultActiveItemId
|
||||
self.activeContentItemIdUpdated = activeContentItemIdUpdated
|
||||
}
|
||||
|
||||
@ -383,6 +386,9 @@ final class EntityKeyboardTopPanelComponent: Component {
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.defaultActiveItemId != rhs.defaultActiveItemId {
|
||||
return false
|
||||
}
|
||||
if lhs.activeContentItemIdUpdated !== rhs.activeContentItemIdUpdated {
|
||||
return false
|
||||
}
|
||||
@ -613,6 +619,10 @@ final class EntityKeyboardTopPanelComponent: Component {
|
||||
}
|
||||
self.component = component
|
||||
|
||||
if let defaultActiveItemId = component.defaultActiveItemId {
|
||||
self.activeContentItemId = defaultActiveItemId
|
||||
}
|
||||
|
||||
let panelEnvironment = environment[EntityKeyboardTopContainerPanelEnvironment.self].value
|
||||
self.environment = panelEnvironment
|
||||
|
||||
@ -639,8 +649,8 @@ final class EntityKeyboardTopPanelComponent: Component {
|
||||
let previousItemFrame = previousItemLayout.containerFrame(at: previousVisibleRange.minIndex)
|
||||
let updatedItemFrame = itemLayout.containerFrame(at: previousVisibleRange.minIndex)
|
||||
|
||||
let previousDistanceToItemFraction = (previousItemFrame.minX - self.scrollView.bounds.minX) / previousItemFrame.width
|
||||
let newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - floor(previousDistanceToItemFraction * updatedItemFrame.width), y: 0.0), size: availableSize)
|
||||
let previousDistanceToItem = (previousItemFrame.minX - self.scrollView.bounds.minX)// / previousItemFrame.width
|
||||
let newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - previousDistanceToItem/* * updatedItemFrame.width)*/, y: 0.0), size: availableSize)
|
||||
updatedBounds = newBounds
|
||||
|
||||
var updatedVisibleBounds = newBounds
|
||||
|
@ -117,6 +117,12 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer {
|
||||
public final class GifPagerContentComponent: Component {
|
||||
public typealias EnvironmentType = (EntityKeyboardChildEnvironment, PagerComponentChildEnvironment)
|
||||
|
||||
public enum Subject: Equatable {
|
||||
case recent
|
||||
case trending
|
||||
case emojiSearch(String)
|
||||
}
|
||||
|
||||
public final class InputInteraction {
|
||||
public let performItemAction: (Item, UIView, CGRect) -> Void
|
||||
|
||||
@ -148,15 +154,18 @@ public final class GifPagerContentComponent: Component {
|
||||
|
||||
public let context: AccountContext
|
||||
public let inputInteraction: InputInteraction
|
||||
public let subject: Subject
|
||||
public let items: [Item]
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
inputInteraction: InputInteraction,
|
||||
subject: Subject,
|
||||
items: [Item]
|
||||
) {
|
||||
self.context = context
|
||||
self.inputInteraction = inputInteraction
|
||||
self.subject = subject
|
||||
self.items = items
|
||||
}
|
||||
|
||||
@ -167,6 +176,9 @@ public final class GifPagerContentComponent: Component {
|
||||
if lhs.inputInteraction !== rhs.inputInteraction {
|
||||
return false
|
||||
}
|
||||
if lhs.subject != rhs.subject {
|
||||
return false
|
||||
}
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
@ -363,7 +375,10 @@ public final class GifPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private let scrollView: UIScrollView
|
||||
private final class ContentScrollView: UIScrollView, PagerExpandableScrollView {
|
||||
}
|
||||
|
||||
private let scrollView: ContentScrollView
|
||||
|
||||
private var visibleItemLayers: [MediaId: ItemLayer] = [:]
|
||||
private var ignoreScrolling: Bool = false
|
||||
@ -374,7 +389,7 @@ public final class GifPagerContentComponent: Component {
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = UIScrollView()
|
||||
self.scrollView = ContentScrollView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
|
24
submodules/TelegramUI/Components/TextNodeWithEntities/BUILD
Normal file
24
submodules/TelegramUI/Components/TextNodeWithEntities/BUILD
Normal file
@ -0,0 +1,24 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "TextNodeWithEntities",
|
||||
module_name = "TextNodeWithEntities",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,466 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import EmojiTextAttachmentView
|
||||
import TextFormat
|
||||
import AccountContext
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
|
||||
private final class InlineStickerItem: Hashable {
|
||||
let emoji: ChatTextInputTextCustomEmojiAttribute
|
||||
let fontSize: CGFloat
|
||||
|
||||
init(emoji: ChatTextInputTextCustomEmojiAttribute, fontSize: CGFloat) {
|
||||
self.emoji = emoji
|
||||
self.fontSize = fontSize
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(emoji.fileId)
|
||||
hasher.combine(self.fontSize)
|
||||
}
|
||||
|
||||
static func ==(lhs: InlineStickerItem, rhs: InlineStickerItem) -> Bool {
|
||||
if lhs.emoji.fileId != rhs.emoji.fileId {
|
||||
return false
|
||||
}
|
||||
if lhs.fontSize != rhs.fontSize {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class TextNodeWithEntities {
|
||||
public final class Arguments {
|
||||
public let context: AccountContext
|
||||
public let cache: AnimationCache
|
||||
public let renderer: MultiAnimationRenderer
|
||||
public let placeholderColor: UIColor
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
cache: AnimationCache,
|
||||
renderer: MultiAnimationRenderer,
|
||||
placeholderColor: UIColor
|
||||
) {
|
||||
self.context = context
|
||||
self.cache = cache
|
||||
self.renderer = renderer
|
||||
self.placeholderColor = placeholderColor
|
||||
}
|
||||
}
|
||||
|
||||
public let textNode: TextNode
|
||||
private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:]
|
||||
|
||||
public var visibilityRect: CGRect? {
|
||||
didSet {
|
||||
if !self.inlineStickerItemLayers.isEmpty && oldValue != self.visibilityRect {
|
||||
for (_, itemLayer) in self.inlineStickerItemLayers {
|
||||
let isItemVisible: Bool
|
||||
if let visibilityRect = self.visibilityRect {
|
||||
if itemLayer.frame.intersects(visibilityRect) {
|
||||
isItemVisible = true
|
||||
} else {
|
||||
isItemVisible = false
|
||||
}
|
||||
} else {
|
||||
isItemVisible = false
|
||||
}
|
||||
itemLayer.isVisibleForAnimations = isItemVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.textNode = TextNode()
|
||||
}
|
||||
|
||||
private init(textNode: TextNode) {
|
||||
self.textNode = textNode
|
||||
}
|
||||
|
||||
public static func asyncLayout(_ maybeNode: TextNodeWithEntities?) -> (TextNodeLayoutArguments) -> (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities) {
|
||||
let makeLayout = TextNode.asyncLayout(maybeNode?.textNode)
|
||||
return { [weak maybeNode] arguments in
|
||||
var updatedString: NSAttributedString?
|
||||
if let sourceString = arguments.attributedString {
|
||||
let string = NSMutableAttributedString(attributedString: sourceString)
|
||||
|
||||
let fullRange = NSRange(location: 0, length: string.length)
|
||||
string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, _ in
|
||||
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||
if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont {
|
||||
string.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: value.stickerPack, fileId: value.fileId), fontSize: font.pointSize), range: range)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
updatedString = string
|
||||
}
|
||||
|
||||
let (layout, apply) = makeLayout(arguments.withAttributedString(updatedString))
|
||||
return (layout, { applyArguments in
|
||||
let result = apply()
|
||||
|
||||
if let maybeNode = maybeNode {
|
||||
if let applyArguments = applyArguments {
|
||||
maybeNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor)
|
||||
}
|
||||
|
||||
return maybeNode
|
||||
} else {
|
||||
let resultNode = TextNodeWithEntities(textNode: result)
|
||||
|
||||
if let applyArguments = applyArguments {
|
||||
resultNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor)
|
||||
}
|
||||
|
||||
return resultNode
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func isItemVisible(itemRect: CGRect) -> Bool {
|
||||
if let visibilityRect = self.visibilityRect {
|
||||
return itemRect.intersects(visibilityRect)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) {
|
||||
var nextIndexById: [Int64: Int] = [:]
|
||||
var validIds: [InlineStickerItemLayer.Key] = []
|
||||
|
||||
if let textLayout = textLayout {
|
||||
for item in textLayout.embeddedItems {
|
||||
if let stickerItem = item.value as? InlineStickerItem {
|
||||
let index: Int
|
||||
if let currentNext = nextIndexById[stickerItem.emoji.fileId] {
|
||||
index = currentNext
|
||||
} else {
|
||||
index = 0
|
||||
}
|
||||
nextIndexById[stickerItem.emoji.fileId] = index + 1
|
||||
let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index)
|
||||
validIds.append(id)
|
||||
|
||||
let itemSize = floor(stickerItem.fontSize * 24.0 / 17.0)
|
||||
|
||||
let itemFrame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 0.0).center, size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0)
|
||||
|
||||
let itemLayer: InlineStickerItemLayer
|
||||
if let current = self.inlineStickerItemLayers[id] {
|
||||
itemLayer = current
|
||||
} else {
|
||||
itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, emoji: stickerItem.emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize))
|
||||
self.inlineStickerItemLayers[id] = itemLayer
|
||||
self.textNode.layer.addSublayer(itemLayer)
|
||||
|
||||
itemLayer.isVisibleForAnimations = self.isItemVisible(itemRect: itemFrame)
|
||||
}
|
||||
|
||||
itemLayer.frame = itemFrame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removeKeys: [InlineStickerItemLayer.Key] = []
|
||||
for (key, itemLayer) in self.inlineStickerItemLayers {
|
||||
if !validIds.contains(key) {
|
||||
removeKeys.append(key)
|
||||
itemLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
for key in removeKeys {
|
||||
self.inlineStickerItemLayers.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ImmediateTextNodeWithEntities: TextNode {
|
||||
public var attributedText: NSAttributedString?
|
||||
public var textAlignment: NSTextAlignment = .natural
|
||||
public var verticalAlignment: TextVerticalAlignment = .top
|
||||
public var truncationType: CTLineTruncationType = .end
|
||||
public var maximumNumberOfLines: Int = 1
|
||||
public var lineSpacing: CGFloat = 0.0
|
||||
public var insets: UIEdgeInsets = UIEdgeInsets()
|
||||
public var textShadowColor: UIColor?
|
||||
public var textStroke: (UIColor, CGFloat)?
|
||||
public var cutout: TextNodeCutout?
|
||||
public var displaySpoilers = false
|
||||
|
||||
public var arguments: TextNodeWithEntities.Arguments?
|
||||
|
||||
private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:]
|
||||
|
||||
public var visibility: Bool = false {
|
||||
didSet {
|
||||
if !self.inlineStickerItemLayers.isEmpty && oldValue != self.visibility {
|
||||
for (_, itemLayer) in self.inlineStickerItemLayers {
|
||||
let isItemVisible: Bool = self.visibility
|
||||
itemLayer.isVisibleForAnimations = isItemVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var truncationMode: NSLineBreakMode {
|
||||
get {
|
||||
switch self.truncationType {
|
||||
case .start:
|
||||
return .byTruncatingHead
|
||||
case .middle:
|
||||
return .byTruncatingMiddle
|
||||
case .end:
|
||||
return .byTruncatingTail
|
||||
@unknown default:
|
||||
return .byTruncatingTail
|
||||
}
|
||||
} set(value) {
|
||||
switch value {
|
||||
case .byTruncatingHead:
|
||||
self.truncationType = .start
|
||||
case .byTruncatingMiddle:
|
||||
self.truncationType = .middle
|
||||
case .byTruncatingTail:
|
||||
self.truncationType = .end
|
||||
default:
|
||||
self.truncationType = .end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
|
||||
public var linkHighlightColor: UIColor?
|
||||
|
||||
public var trailingLineWidth: CGFloat?
|
||||
|
||||
var constrainedSize: CGSize?
|
||||
|
||||
public var highlightAttributeAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.updateInteractiveActions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
|
||||
public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
|
||||
|
||||
private func processedAttributedText() -> NSAttributedString? {
|
||||
var updatedString: NSAttributedString?
|
||||
if let sourceString = self.attributedText {
|
||||
let string = NSMutableAttributedString(attributedString: sourceString)
|
||||
|
||||
let fullRange = NSRange(location: 0, length: string.length)
|
||||
string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, _ in
|
||||
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||
if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont {
|
||||
string.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: value.stickerPack, fileId: value.fileId), fontSize: font.pointSize), range: range)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
updatedString = string
|
||||
}
|
||||
return updatedString
|
||||
}
|
||||
|
||||
public func updateLayout(_ constrainedSize: CGSize) -> CGSize {
|
||||
self.constrainedSize = constrainedSize
|
||||
|
||||
let makeLayout = TextNode.asyncLayout(self)
|
||||
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.processedAttributedText(), backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, textShadowColor: self.textShadowColor, textStroke: self.textStroke, displaySpoilers: self.displaySpoilers))
|
||||
|
||||
let _ = apply()
|
||||
|
||||
if let arguments = self.arguments {
|
||||
self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor)
|
||||
}
|
||||
|
||||
if layout.numberOfLines > 1 {
|
||||
self.trailingLineWidth = layout.trailingLineWidth
|
||||
} else {
|
||||
self.trailingLineWidth = nil
|
||||
}
|
||||
return layout.size
|
||||
}
|
||||
|
||||
private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) {
|
||||
var nextIndexById: [Int64: Int] = [:]
|
||||
var validIds: [InlineStickerItemLayer.Key] = []
|
||||
|
||||
if let textLayout = textLayout {
|
||||
for item in textLayout.embeddedItems {
|
||||
if let stickerItem = item.value as? InlineStickerItem {
|
||||
let index: Int
|
||||
if let currentNext = nextIndexById[stickerItem.emoji.fileId] {
|
||||
index = currentNext
|
||||
} else {
|
||||
index = 0
|
||||
}
|
||||
nextIndexById[stickerItem.emoji.fileId] = index + 1
|
||||
let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index)
|
||||
validIds.append(id)
|
||||
|
||||
let itemSize = floor(stickerItem.fontSize * 24.0 / 17.0)
|
||||
|
||||
let itemFrame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 0.0).center, size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0)
|
||||
|
||||
let itemLayer: InlineStickerItemLayer
|
||||
if let current = self.inlineStickerItemLayers[id] {
|
||||
itemLayer = current
|
||||
} else {
|
||||
itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, emoji: stickerItem.emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize))
|
||||
self.inlineStickerItemLayers[id] = itemLayer
|
||||
self.layer.addSublayer(itemLayer)
|
||||
|
||||
itemLayer.isVisibleForAnimations = self.visibility
|
||||
}
|
||||
|
||||
itemLayer.frame = itemFrame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removeKeys: [InlineStickerItemLayer.Key] = []
|
||||
for (key, itemLayer) in self.inlineStickerItemLayers {
|
||||
if !validIds.contains(key) {
|
||||
removeKeys.append(key)
|
||||
itemLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
for key in removeKeys {
|
||||
self.inlineStickerItemLayers.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo {
|
||||
self.constrainedSize = constrainedSize
|
||||
|
||||
let makeLayout = TextNode.asyncLayout(self)
|
||||
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.processedAttributedText(), backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, displaySpoilers: self.displaySpoilers))
|
||||
|
||||
let _ = apply()
|
||||
|
||||
if let arguments = self.arguments {
|
||||
self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor)
|
||||
}
|
||||
|
||||
return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated, numberOfLines: layout.numberOfLines)
|
||||
}
|
||||
|
||||
public func updateLayoutFullInfo(_ constrainedSize: CGSize) -> TextNodeLayout {
|
||||
self.constrainedSize = constrainedSize
|
||||
|
||||
let makeLayout = TextNode.asyncLayout(self)
|
||||
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.processedAttributedText(), backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, displaySpoilers: self.displaySpoilers))
|
||||
|
||||
let _ = apply()
|
||||
|
||||
if let arguments = self.arguments {
|
||||
self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor)
|
||||
}
|
||||
|
||||
return layout
|
||||
}
|
||||
|
||||
public func redrawIfPossible() {
|
||||
if let constrainedSize = self.constrainedSize {
|
||||
let _ = self.updateLayout(constrainedSize)
|
||||
}
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.updateInteractiveActions()
|
||||
}
|
||||
|
||||
private func updateInteractiveActions() {
|
||||
if self.highlightAttributeAction != nil {
|
||||
if self.tapRecognizer == nil {
|
||||
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:)))
|
||||
tapRecognizer.highlight = { [weak self] point in
|
||||
if let strongSelf = self {
|
||||
var rects: [CGRect]?
|
||||
if let point = point {
|
||||
if let (index, attributes) = strongSelf.attributesAtPoint(CGPoint(x: point.x, y: point.y)) {
|
||||
if let selectedAttribute = strongSelf.highlightAttributeAction?(attributes) {
|
||||
let initialRects = strongSelf.lineAndAttributeRects(name: selectedAttribute.rawValue, at: index)
|
||||
if let initialRects = initialRects, case .center = strongSelf.textAlignment {
|
||||
var mappedRects: [CGRect] = []
|
||||
for i in 0 ..< initialRects.count {
|
||||
let lineRect = initialRects[i].0
|
||||
var itemRect = initialRects[i].1
|
||||
itemRect.origin.x = floor((strongSelf.bounds.size.width - lineRect.width) / 2.0) + itemRect.origin.x
|
||||
mappedRects.append(itemRect)
|
||||
}
|
||||
rects = mappedRects
|
||||
} else {
|
||||
rects = strongSelf.attributeRects(name: selectedAttribute.rawValue, at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let rects = rects {
|
||||
let linkHighlightingNode: LinkHighlightingNode
|
||||
if let current = strongSelf.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
} else {
|
||||
linkHighlightingNode = LinkHighlightingNode(color: strongSelf.linkHighlightColor ?? .clear)
|
||||
strongSelf.linkHighlightingNode = linkHighlightingNode
|
||||
strongSelf.addSubnode(linkHighlightingNode)
|
||||
}
|
||||
linkHighlightingNode.frame = strongSelf.bounds
|
||||
linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) })
|
||||
} else if let linkHighlightingNode = strongSelf.linkHighlightingNode {
|
||||
strongSelf.linkHighlightingNode = nil
|
||||
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
||||
linkHighlightingNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
self.view.addGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
} else if let tapRecognizer = self.tapRecognizer {
|
||||
self.tapRecognizer = nil
|
||||
self.view.removeGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
if let (index, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
|
||||
self.tapAttributeAction?(attributes, index)
|
||||
}
|
||||
case .longTap:
|
||||
if let (index, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
|
||||
self.longTapAttributeAction?(attributes, index)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
@ -918,7 +918,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
wasDraggingKeyboard = true
|
||||
}
|
||||
var wasDraggingInputNode = false
|
||||
if let derivedLayoutState = self.derivedLayoutState, let inputNodeHeight = derivedLayoutState.inputNodeHeight, !inputNodeHeight.isZero, let upperInputPositionBound = derivedLayoutState.upperInputPositionBound {
|
||||
if let derivedLayoutState = self.derivedLayoutState, let inputNodeHeight = derivedLayoutState.inputNodeHeight, !inputNodeHeight.isZero, let upperInputPositionBound = derivedLayoutState.upperInputPositionBound {
|
||||
let normalizedHeight = max(0.0, layout.size.height - upperInputPositionBound)
|
||||
if normalizedHeight < inputNodeHeight {
|
||||
wasDraggingInputNode = true
|
||||
@ -1113,7 +1113,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
var accessoryPanelSize: CGSize?
|
||||
var immediatelyLayoutAccessoryPanelAndAnimateAppearance = false
|
||||
if let accessoryPanelNode = accessoryPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.accessoryPanelNode, interfaceInteraction: self.interfaceInteraction) {
|
||||
if let accessoryPanelNode = accessoryPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.accessoryPanelNode, chatControllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction) {
|
||||
accessoryPanelSize = accessoryPanelNode.measure(CGSize(width: layout.size.width, height: layout.size.height))
|
||||
|
||||
accessoryPanelNode.updateState(size: layout.size, inset: layout.safeInsets.left, interfaceState: self.chatPresentationInterfaceState)
|
||||
@ -2679,7 +2679,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
let keyboardGestureBeginLocation = location
|
||||
let accessoryHeight = self.getWindowInputAccessoryHeight()
|
||||
if let inputHeight = derivedLayoutState.inputNodeHeight, !inputHeight.isZero, keyboardGestureBeginLocation.y < validLayout.size.height - inputHeight - accessoryHeight {
|
||||
if let inputHeight = derivedLayoutState.inputNodeHeight, !inputHeight.isZero, keyboardGestureBeginLocation.y < validLayout.size.height - inputHeight - accessoryHeight, !self.inputPanelContainerNode.stableIsExpanded {
|
||||
var enableGesture = true
|
||||
if let view = self.view.hitTest(location, with: nil) {
|
||||
if doesViewTreeDisableInteractiveTransitionGestureRecognizer(view) {
|
||||
|
@ -18,18 +18,21 @@ import PagerComponent
|
||||
|
||||
final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
struct InputData: Equatable {
|
||||
let emoji: EmojiPagerContentComponent
|
||||
let stickers: EmojiPagerContentComponent
|
||||
let gifs: GifPagerContentComponent
|
||||
var emoji: EmojiPagerContentComponent
|
||||
var stickers: EmojiPagerContentComponent
|
||||
var gifs: GifPagerContentComponent
|
||||
var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
|
||||
|
||||
init(
|
||||
emoji: EmojiPagerContentComponent,
|
||||
stickers: EmojiPagerContentComponent,
|
||||
gifs: GifPagerContentComponent
|
||||
gifs: GifPagerContentComponent,
|
||||
availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
|
||||
) {
|
||||
self.emoji = emoji
|
||||
self.stickers = stickers
|
||||
self.gifs = gifs
|
||||
self.availableGifSearchEmojies = availableGifSearchEmojies
|
||||
}
|
||||
}
|
||||
|
||||
@ -419,7 +422,39 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
)
|
||||
}
|
||||
|
||||
let gifItems: Signal<GifPagerContentComponent, NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|
||||
let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App())
|
||||
|> map { appConfiguration -> [String] in
|
||||
let defaultReactions: [String] = ["👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"]
|
||||
|
||||
guard let data = appConfiguration.data, let emojis = data["gif_search_emojies"] as? [String] else {
|
||||
return defaultReactions
|
||||
}
|
||||
return emojis
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false)
|
||||
|> map { animatedEmoji -> [String: [StickerPackItem]] in
|
||||
var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
|
||||
switch animatedEmoji {
|
||||
case let .result(_, items, _):
|
||||
for item in items {
|
||||
if let emoji = item.getStringRepresentationsOfIndexKeys().first {
|
||||
animatedEmojiStickers[emoji.basicEmoji.0] = [item]
|
||||
let strippedEmoji = emoji.basicEmoji.0.strippedEmoji
|
||||
if animatedEmojiStickers[strippedEmoji] == nil {
|
||||
animatedEmojiStickers[strippedEmoji] = [item]
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return animatedEmojiStickers
|
||||
}
|
||||
|
||||
// We are intentionally not subscribing to the recent gif updates here
|
||||
let gifItems: Signal<GifPagerContentComponent, NoError> = context.engine.data.get(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|
||||
|> map { savedGifs -> GifPagerContentComponent in
|
||||
var items: [GifPagerContentComponent.Item] = []
|
||||
for gifItem in savedGifs {
|
||||
@ -430,6 +465,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
return GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: .recent,
|
||||
items: items
|
||||
)
|
||||
}
|
||||
@ -437,13 +473,23 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
return combineLatest(queue: .mainQueue(),
|
||||
emojiItems,
|
||||
stickerItems,
|
||||
gifItems
|
||||
gifItems,
|
||||
reactions,
|
||||
animatedEmojiStickers
|
||||
)
|
||||
|> map { emoji, stickers, gifs -> InputData in
|
||||
|> map { emoji, stickers, gifs, reactions, animatedEmojiStickers -> InputData in
|
||||
var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] = []
|
||||
for reaction in reactions {
|
||||
if let file = animatedEmojiStickers[reaction]?.first?.file {
|
||||
availableGifSearchEmojies.append(EntityKeyboardComponent.GifSearchEmoji(emoji: reaction, file: file, title: reaction))
|
||||
}
|
||||
}
|
||||
|
||||
return InputData(
|
||||
emoji: emoji,
|
||||
stickers: stickers,
|
||||
gifs: gifs
|
||||
gifs: gifs,
|
||||
availableGifSearchEmojies: availableGifSearchEmojies
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -470,6 +516,13 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|
||||
private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)?
|
||||
|
||||
private var gifMode: GifPagerContentComponent.Subject = .recent {
|
||||
didSet {
|
||||
self.gifModeSubject.set(self.gifMode)
|
||||
}
|
||||
}
|
||||
private let gifModeSubject: ValuePromise<GifPagerContentComponent.Subject>
|
||||
|
||||
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction) {
|
||||
self.context = context
|
||||
self.currentInputData = currentInputData
|
||||
@ -479,17 +532,25 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|
||||
self.entityKeyboardView = ComponentHostView<Empty>()
|
||||
|
||||
self.gifModeSubject = ValuePromise<GifPagerContentComponent.Subject>(self.gifMode, ignoreRepeated: true)
|
||||
|
||||
super.init()
|
||||
|
||||
self.view.addSubview(self.entityKeyboardView)
|
||||
|
||||
self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer()
|
||||
|
||||
self.inputDataDisposable = (updatedInputData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] inputData in
|
||||
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
|
||||
updatedInputData,
|
||||
self.updatedGifs()
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] inputData, gifs in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var inputData = inputData
|
||||
inputData.gifs = gifs
|
||||
|
||||
strongSelf.currentInputData = inputData
|
||||
strongSelf.performLayout()
|
||||
})
|
||||
@ -531,6 +592,92 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
self.inputDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func updatedGifs() -> Signal<GifPagerContentComponent, NoError> {
|
||||
let gifInputInteraction = GifPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak controllerInteraction] item, view, rect in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
return
|
||||
}
|
||||
let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false)
|
||||
}
|
||||
)
|
||||
|
||||
let context = self.context
|
||||
let trendingGifs = self.trendingGifsPromise.get()
|
||||
let updatedGifs = self.gifModeSubject.get()
|
||||
|> mapToSignal { subject -> Signal<GifPagerContentComponent, NoError> in
|
||||
switch subject {
|
||||
case .recent:
|
||||
let gifItems: Signal<GifPagerContentComponent, NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|
||||
|> map { savedGifs -> GifPagerContentComponent in
|
||||
var items: [GifPagerContentComponent.Item] = []
|
||||
for gifItem in savedGifs {
|
||||
items.append(GifPagerContentComponent.Item(
|
||||
file: gifItem.contents.get(RecentMediaItem.self)!.media
|
||||
))
|
||||
}
|
||||
return GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: subject,
|
||||
items: items
|
||||
)
|
||||
}
|
||||
return gifItems
|
||||
case .trending:
|
||||
return trendingGifs
|
||||
|> map { trendingGifs -> GifPagerContentComponent in
|
||||
var items: [GifPagerContentComponent.Item] = []
|
||||
|
||||
if let trendingGifs = trendingGifs {
|
||||
for file in trendingGifs.files {
|
||||
items.append(GifPagerContentComponent.Item(
|
||||
file: file.file.media
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: subject,
|
||||
items: items
|
||||
)
|
||||
}
|
||||
case let .emojiSearch(query):
|
||||
return paneGifSearchForQuery(context: context, query: query, offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil)
|
||||
|> map { result -> GifPagerContentComponent in
|
||||
var items: [GifPagerContentComponent.Item] = []
|
||||
|
||||
/*let canLoadMore: Bool
|
||||
if let result = result {
|
||||
canLoadMore = !result.isComplete
|
||||
} else {
|
||||
canLoadMore = true
|
||||
}*/
|
||||
|
||||
if let result = result {
|
||||
for file in result.files {
|
||||
items.append(GifPagerContentComponent.Item(
|
||||
file: file.file.media
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: subject,
|
||||
items: items
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .single(self.currentInputData.gifs)
|
||||
|> then(updatedGifs)
|
||||
}
|
||||
|
||||
func markInputCollapsed() {
|
||||
self.isMarkInputCollapsed = true
|
||||
}
|
||||
@ -574,6 +721,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
emojiContent: self.currentInputData.emoji,
|
||||
stickerContent: self.currentInputData.stickers,
|
||||
gifContent: self.currentInputData.gifs,
|
||||
availableGifSearchEmojies: self.currentInputData.availableGifSearchEmojies,
|
||||
defaultToEmojiTab: self.defaultToEmojiTab,
|
||||
externalTopPanelContainer: self.externalTopPanelContainerImpl,
|
||||
topPanelExtensionUpdated: { [weak self] topPanelExtension, transition in
|
||||
@ -603,6 +751,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
return .text
|
||||
}
|
||||
},
|
||||
switchToGifSubject: { [weak self] subject in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.gifModeSubject.set(subject)
|
||||
},
|
||||
makeSearchContainerNode: { content in
|
||||
let mappedMode: ChatMediaInputSearchMode
|
||||
switch content {
|
||||
|
@ -5,7 +5,7 @@ import TelegramCore
|
||||
import AccountContext
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: AccessoryPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? {
|
||||
func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: AccessoryPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? {
|
||||
if let _ = chatPresentationInterfaceState.interfaceState.selectionState {
|
||||
return nil
|
||||
}
|
||||
@ -70,7 +70,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
|
||||
replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
|
||||
return replyPanelNode
|
||||
} else {
|
||||
let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat)
|
||||
let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer)
|
||||
panelNode.interfaceInteraction = interfaceInteraction
|
||||
return panelNode
|
||||
}
|
||||
|
@ -1017,7 +1017,17 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
|
||||
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId {
|
||||
} else {
|
||||
replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, item.message, CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments(
|
||||
presentationData: item.presentationData,
|
||||
strings: item.presentationData.strings,
|
||||
context: item.context,
|
||||
type: .standalone,
|
||||
message: replyMessage,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
|
||||
))
|
||||
}
|
||||
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
|
||||
replyMarkup = attribute
|
||||
|
@ -530,13 +530,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
contentNode.visibility = mapVisibility(self.visibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
|
||||
}
|
||||
|
||||
/*switch self.visibility {
|
||||
case let .visible(_, subRect):
|
||||
let topEdge = self.bounds.height - self.insets.top - (subRect.origin.y + subRect.height)
|
||||
self.debugNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topEdge), size: CGSize(width: 100.0, height: 2.0))
|
||||
case .none:
|
||||
break
|
||||
}*/
|
||||
if let replyInfoNode = self.replyInfoNode {
|
||||
replyInfoNode.visibility = self.visibility != .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1039,7 +1035,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
|
||||
adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
|
||||
forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode),
|
||||
replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode),
|
||||
replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, () -> ChatMessageReplyInfoNode),
|
||||
actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)),
|
||||
reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)),
|
||||
mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)),
|
||||
@ -1802,7 +1798,17 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
} else {
|
||||
headerSize.height += 2.0
|
||||
}
|
||||
let sizeAndApply = replyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .bubble(incoming: incoming), replyMessage, item.message, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude))
|
||||
let sizeAndApply = replyInfoLayout(ChatMessageReplyInfoNode.Arguments(
|
||||
presentationData: item.presentationData,
|
||||
strings: item.presentationData.strings,
|
||||
context: item.context,
|
||||
type: .bubble(incoming: incoming),
|
||||
message: replyMessage,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
|
||||
))
|
||||
replyInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() })
|
||||
|
||||
replyInfoOriginY = headerSize.height
|
||||
@ -2489,6 +2495,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
if replyInfoNode.supernode == nil {
|
||||
strongSelf.clippingNode.addSubnode(replyInfoNode)
|
||||
animateFrame = false
|
||||
|
||||
replyInfoNode.visibility = strongSelf.visibility != .none
|
||||
}
|
||||
let previousReplyInfoNodeFrame = replyInfoNode.frame
|
||||
replyInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + replyInfoOriginY), size: replyInfoSizeApply.0)
|
||||
|
@ -458,7 +458,17 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
||||
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
|
||||
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId {
|
||||
} else {
|
||||
replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, item.message, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments(
|
||||
presentationData: item.presentationData,
|
||||
strings: item.presentationData.strings,
|
||||
context: item.context,
|
||||
type: .standalone,
|
||||
message: replyMessage,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
|
||||
))
|
||||
}
|
||||
} else if let _ = attribute as? InlineBotMessageAttribute {
|
||||
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
|
||||
|
@ -196,10 +196,10 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
||||
}
|
||||
if messageEntities?.count == 0 {
|
||||
messageEntities = nil
|
||||
messageText = textString
|
||||
messageText = textString.string
|
||||
}
|
||||
} else {
|
||||
messageText = textString
|
||||
messageText = textString.string
|
||||
}
|
||||
} else if item.messages.count > 1, let peer = item.messages[0].peers[item.messages[0].id.peerId] {
|
||||
var displayAuthor = true
|
||||
|
@ -12,6 +12,9 @@ import PhotoResources
|
||||
import TelegramStringFormatting
|
||||
import TextFormat
|
||||
import InvisibleInkDustNode
|
||||
import TextNodeWithEntities
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
|
||||
enum ChatMessageReplyInfoType {
|
||||
case bubble(incoming: Bool)
|
||||
@ -19,10 +22,52 @@ enum ChatMessageReplyInfoType {
|
||||
}
|
||||
|
||||
class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
class Arguments {
|
||||
let presentationData: ChatPresentationData
|
||||
let strings: PresentationStrings
|
||||
let context: AccountContext
|
||||
let type: ChatMessageReplyInfoType
|
||||
let message: Message
|
||||
let parentMessage: Message
|
||||
let constrainedSize: CGSize
|
||||
let animationCache: AnimationCache?
|
||||
let animationRenderer: MultiAnimationRenderer?
|
||||
|
||||
init(
|
||||
presentationData: ChatPresentationData,
|
||||
strings: PresentationStrings,
|
||||
context: AccountContext,
|
||||
type: ChatMessageReplyInfoType,
|
||||
message: Message,
|
||||
parentMessage: Message,
|
||||
constrainedSize: CGSize,
|
||||
animationCache: AnimationCache?,
|
||||
animationRenderer: MultiAnimationRenderer?
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.strings = strings
|
||||
self.context = context
|
||||
self.type = type
|
||||
self.message = message
|
||||
self.parentMessage = parentMessage
|
||||
self.constrainedSize = constrainedSize
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = animationRenderer
|
||||
}
|
||||
}
|
||||
|
||||
var visibility: Bool = false {
|
||||
didSet {
|
||||
if self.visibility != oldValue {
|
||||
self.textNode?.visibilityRect = self.visibility ? CGRect.infinite : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let contentNode: ASDisplayNode
|
||||
private let lineNode: ASImageNode
|
||||
private var titleNode: TextNode?
|
||||
private var textNode: TextNode?
|
||||
private var textNode: TextNodeWithEntities?
|
||||
private var dustNode: InvisibleInkDustNode?
|
||||
private var imageNode: TransformImageNode?
|
||||
private var previousMediaReference: AnyMediaReference?
|
||||
@ -45,31 +90,33 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
self.contentNode.addSubnode(self.lineNode)
|
||||
}
|
||||
|
||||
class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ theme: ChatPresentationData, _ strings: PresentationStrings, _ context: AccountContext, _ type: ChatMessageReplyInfoType, _ message: Message, _ parentMessage: Message, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode) {
|
||||
class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, () -> ChatMessageReplyInfoNode) {
|
||||
let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode)
|
||||
let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode)
|
||||
let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode)
|
||||
let imageNodeLayout = TransformImageNode.asyncLayout(maybeNode?.imageNode)
|
||||
let previousMediaReference = maybeNode?.previousMediaReference
|
||||
|
||||
return { presentationData, strings, context, type, message, parentMessage, constrainedSize in
|
||||
let fontSize = floor(presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
|
||||
return { arguments in
|
||||
//presentationData, strings, context, type, message, parentMessage, constrainedSize
|
||||
|
||||
let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
|
||||
let titleFont = Font.medium(fontSize)
|
||||
let textFont = Font.regular(fontSize)
|
||||
|
||||
let author = message.effectiveAuthor
|
||||
var titleString = author.flatMap(EnginePeer.init)?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) ?? strings.User_DeletedAccount
|
||||
let author = arguments.message.effectiveAuthor
|
||||
var titleString = author.flatMap(EnginePeer.init)?.displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) ?? arguments.strings.User_DeletedAccount
|
||||
|
||||
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) || parentMessage.forwardInfo != nil {
|
||||
if let forwardInfo = arguments.message.forwardInfo, forwardInfo.flags.contains(.isImported) || arguments.parentMessage.forwardInfo != nil {
|
||||
if let author = forwardInfo.author {
|
||||
titleString = EnginePeer(author).displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
titleString = EnginePeer(author).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
|
||||
} else if let authorSignature = forwardInfo.authorSignature {
|
||||
titleString = authorSignature
|
||||
}
|
||||
}
|
||||
|
||||
let (textString, isMedia, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: context.account.peerId)
|
||||
let (textString, isMedia, isText) = descriptionStringForMessage(contentSettings: arguments.context.currentContentSettings.with { $0 }, message: EngineMessage(arguments.message), strings: arguments.strings, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, dateTimeFormat: arguments.presentationData.dateTimeFormat, accountPeerId: arguments.context.account.peerId)
|
||||
|
||||
let placeholderColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
|
||||
let placeholderColor: UIColor = arguments.message.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : arguments.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
|
||||
let titleColor: UIColor
|
||||
let lineImage: UIImage?
|
||||
let textColor: UIColor
|
||||
@ -77,11 +124,11 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
|
||||
var authorNameColor: UIColor?
|
||||
|
||||
if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(parentMessage.id.peerId.namespace) && author?.id.namespace == Namespaces.Peer.CloudUser {
|
||||
if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(arguments.parentMessage.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 {
|
||||
switch arguments.presentationData.theme.theme.name {
|
||||
case .builtin(.nightAccent), .builtin(.night):
|
||||
dimColors = true
|
||||
default:
|
||||
@ -97,21 +144,21 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
switch type {
|
||||
switch arguments.type {
|
||||
case let .bubble(incoming):
|
||||
titleColor = incoming ? (authorNameColor ?? presentationData.theme.theme.chat.message.incoming.accentTextColor) : presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
||||
lineImage = incoming ? (authorNameColor.flatMap({ PresentationResourcesChat.chatBubbleVerticalLineImage(color: $0) }) ?? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(presentationData.theme.theme)) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(presentationData.theme.theme)
|
||||
titleColor = incoming ? (authorNameColor ?? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor) : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
||||
lineImage = incoming ? (authorNameColor.flatMap({ PresentationResourcesChat.chatBubbleVerticalLineImage(color: $0) }) ?? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(arguments.presentationData.theme.theme)) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(arguments.presentationData.theme.theme)
|
||||
if isMedia {
|
||||
textColor = incoming ? presentationData.theme.theme.chat.message.incoming.secondaryTextColor : presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
|
||||
textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
|
||||
} else {
|
||||
textColor = incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor
|
||||
textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.primaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.primaryTextColor
|
||||
}
|
||||
dustColor = incoming ? presentationData.theme.theme.chat.message.incoming.secondaryTextColor : presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
|
||||
dustColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
|
||||
case .standalone:
|
||||
let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
|
||||
titleColor = serviceColor.primaryText
|
||||
|
||||
let graphics = PresentationResourcesChat.additionalGraphics(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners)
|
||||
let graphics = PresentationResourcesChat.additionalGraphics(arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners)
|
||||
lineImage = graphics.chatServiceVerticalLineImage
|
||||
textColor = titleColor
|
||||
dustColor = titleColor
|
||||
@ -120,20 +167,22 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
|
||||
let messageText: NSAttributedString
|
||||
if isText {
|
||||
let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in
|
||||
let entities = (arguments.message.textEntitiesAttribute?.entities ?? []).filter { entity in
|
||||
if case .Spoiler = entity.type {
|
||||
return true
|
||||
} else if case .CustomEmoji = entity.type {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if entities.count > 0 {
|
||||
messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||
messageText = stringWithAppliedEntities(trimToLineCount(arguments.message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||
} else {
|
||||
messageText = NSAttributedString(string: textString, font: textFont, textColor: textColor)
|
||||
messageText = NSAttributedString(string: textString.string, font: textFont, textColor: textColor)
|
||||
}
|
||||
} else {
|
||||
messageText = NSAttributedString(string: textString, font: textFont, textColor: textColor)
|
||||
messageText = NSAttributedString(string: textString.string, font: textFont, textColor: textColor)
|
||||
}
|
||||
|
||||
var leftInset: CGFloat = 11.0
|
||||
@ -142,16 +191,16 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
var updatedMediaReference: AnyMediaReference?
|
||||
var imageDimensions: CGSize?
|
||||
var hasRoundImage = false
|
||||
if !message.containsSecretMedia {
|
||||
for media in message.media {
|
||||
if !arguments.message.containsSecretMedia {
|
||||
for media in arguments.message.media {
|
||||
if let image = media as? TelegramMediaImage {
|
||||
updatedMediaReference = .message(message: MessageReference(message), media: image)
|
||||
updatedMediaReference = .message(message: MessageReference(arguments.message), media: image)
|
||||
if let representation = largestRepresentationForPhoto(image) {
|
||||
imageDimensions = representation.dimensions.cgSize
|
||||
}
|
||||
break
|
||||
} else if let file = media as? TelegramMediaFile, file.isVideo && !file.isVideoSticker {
|
||||
updatedMediaReference = .message(message: MessageReference(message), media: file)
|
||||
updatedMediaReference = .message(message: MessageReference(arguments.message), media: file)
|
||||
|
||||
if let dimensions = file.dimensions {
|
||||
imageDimensions = dimensions.cgSize
|
||||
@ -168,12 +217,12 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
|
||||
var imageTextInset: CGFloat = 0.0
|
||||
if let _ = imageDimensions {
|
||||
imageTextInset += floor(presentationData.fontSize.baseDisplaySize * 32.0 / 17.0)
|
||||
imageTextInset += floor(arguments.presentationData.fontSize.baseDisplaySize * 32.0 / 17.0)
|
||||
}
|
||||
|
||||
let maximumTextWidth = max(0.0, constrainedSize.width - imageTextInset)
|
||||
let maximumTextWidth = max(0.0, arguments.constrainedSize.width - imageTextInset)
|
||||
|
||||
let contrainedTextSize = CGSize(width: maximumTextWidth, height: constrainedSize.height)
|
||||
let contrainedTextSize = CGSize(width: maximumTextWidth, height: arguments.constrainedSize.height)
|
||||
|
||||
let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
|
||||
|
||||
@ -206,12 +255,12 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
if let updatedMediaReference = updatedMediaReference, mediaUpdated && imageDimensions != nil {
|
||||
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
|
||||
updateImageSignal = chatMessagePhotoThumbnail(account: context.account, photoReference: imageReference)
|
||||
updateImageSignal = chatMessagePhotoThumbnail(account: arguments.context.account, photoReference: imageReference)
|
||||
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
|
||||
if fileReference.media.isVideo {
|
||||
updateImageSignal = chatMessageVideoThumbnail(account: context.account, fileReference: fileReference)
|
||||
updateImageSignal = chatMessageVideoThumbnail(account: arguments.context.account, fileReference: fileReference)
|
||||
} else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {
|
||||
updateImageSignal = chatWebpageSnippetFile(account: context.account, mediaReference: fileReference.abstract, representation: iconImageRepresentation)
|
||||
updateImageSignal = chatWebpageSnippetFile(account: arguments.context.account, mediaReference: fileReference.abstract, representation: iconImageRepresentation)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -228,11 +277,16 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
|
||||
node.previousMediaReference = updatedMediaReference
|
||||
|
||||
node.titleNode?.displaysAsynchronously = !presentationData.isPreview
|
||||
node.textNode?.displaysAsynchronously = !presentationData.isPreview
|
||||
node.titleNode?.displaysAsynchronously = !arguments.presentationData.isPreview
|
||||
node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview
|
||||
|
||||
let titleNode = titleApply()
|
||||
let textNode = textApply()
|
||||
var textArguments: TextNodeWithEntities.Arguments?
|
||||
if let cache = arguments.animationCache, let renderer = arguments.animationRenderer {
|
||||
textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: placeholderColor)
|
||||
}
|
||||
let textNode = textApply(textArguments)
|
||||
textNode.visibilityRect = node.visibility ? CGRect.infinite : nil
|
||||
|
||||
if node.titleNode == nil {
|
||||
titleNode.isUserInteractionEnabled = false
|
||||
@ -241,9 +295,9 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
if node.textNode == nil {
|
||||
textNode.isUserInteractionEnabled = false
|
||||
textNode.textNode.isUserInteractionEnabled = false
|
||||
node.textNode = textNode
|
||||
node.contentNode.addSubnode(textNode)
|
||||
node.contentNode.addSubnode(textNode.textNode)
|
||||
}
|
||||
|
||||
if let applyImage = applyImage {
|
||||
@ -262,12 +316,12 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
imageNode.removeFromSupernode()
|
||||
node.imageNode = nil
|
||||
}
|
||||
node.imageNode?.captureProtected = message.isCopyProtected()
|
||||
node.imageNode?.captureProtected = arguments.message.isCopyProtected()
|
||||
|
||||
titleNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0, y: spacing - textInsets.top + 1.0), size: titleLayout.size)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top - 2.0), size: textLayout.size)
|
||||
textNode.frame = textFrame
|
||||
textNode.textNode.frame = textFrame
|
||||
|
||||
if !textLayout.spoilers.isEmpty {
|
||||
let dustNode: InvisibleInkDustNode
|
||||
@ -277,7 +331,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
dustNode = InvisibleInkDustNode(textNode: nil)
|
||||
dustNode.isUserInteractionEnabled = false
|
||||
node.dustNode = dustNode
|
||||
node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode)
|
||||
node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode.textNode)
|
||||
}
|
||||
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
||||
dustNode.update(size: dustNode.frame.size, color: dustColor, textColor: dustColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
||||
@ -344,16 +398,16 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
|
||||
if let textNode = self.textNode {
|
||||
let offset = CGPoint(
|
||||
x: localRect.minX + sourceReplyPanel.textNode.frame.minX - textNode.frame.minX,
|
||||
y: localRect.minY + sourceReplyPanel.textNode.frame.midY - textNode.frame.midY
|
||||
x: localRect.minX + sourceReplyPanel.textNode.frame.minX - textNode.textNode.frame.minX,
|
||||
y: localRect.minY + sourceReplyPanel.textNode.frame.midY - textNode.textNode.frame.midY
|
||||
)
|
||||
|
||||
transition.horizontal.animatePositionAdditive(node: textNode, offset: CGPoint(x: offset.x, y: 0.0))
|
||||
transition.vertical.animatePositionAdditive(node: textNode, offset: CGPoint(x: 0.0, y: offset.y))
|
||||
transition.horizontal.animatePositionAdditive(node: textNode.textNode, offset: CGPoint(x: offset.x, y: 0.0))
|
||||
transition.vertical.animatePositionAdditive(node: textNode.textNode, offset: CGPoint(x: 0.0, y: offset.y))
|
||||
|
||||
sourceParentNode.addSubnode(sourceReplyPanel.textNode)
|
||||
|
||||
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
|
||||
sourceReplyPanel.textNode.frame = sourceReplyPanel.textNode.frame
|
||||
.offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y)
|
||||
|
@ -570,7 +570,17 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
|
||||
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId {
|
||||
} else {
|
||||
replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, item.message, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments(
|
||||
presentationData: item.presentationData,
|
||||
strings: item.presentationData.strings,
|
||||
context: item.context,
|
||||
type: .standalone,
|
||||
message: replyMessage,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
|
||||
))
|
||||
}
|
||||
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
|
||||
replyMarkup = attribute
|
||||
|
@ -19,6 +19,7 @@ import AnimationCache
|
||||
import LottieAnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import EmojiTextAttachmentView
|
||||
import TextNodeWithEntities
|
||||
|
||||
private final class CachedChatMessageText {
|
||||
let text: String
|
||||
@ -46,27 +47,8 @@ private final class CachedChatMessageText {
|
||||
}
|
||||
}
|
||||
|
||||
private final class InlineStickerItem: Hashable {
|
||||
let emoji: ChatTextInputTextCustomEmojiAttribute
|
||||
|
||||
init(emoji: ChatTextInputTextCustomEmojiAttribute) {
|
||||
self.emoji = emoji
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(emoji.fileId)
|
||||
}
|
||||
|
||||
static func ==(lhs: InlineStickerItem, rhs: InlineStickerItem) -> Bool {
|
||||
if lhs.emoji.fileId != rhs.emoji.fileId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let textNode: TextNode
|
||||
private let textNode: TextNodeWithEntities
|
||||
private var spoilerTextNode: TextNode?
|
||||
private var dustNode: InvisibleInkDustNode?
|
||||
|
||||
@ -76,42 +58,27 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private var textSelectionNode: TextSelectionNode?
|
||||
|
||||
private var textHighlightingNodes: [LinkHighlightingNode] = []
|
||||
private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:]
|
||||
|
||||
private var cachedChatMessageText: CachedChatMessageText?
|
||||
|
||||
private var isVisibleForAnimations: Bool {
|
||||
return self.visibility != .none
|
||||
}
|
||||
|
||||
override var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
if !self.inlineStickerItemLayers.isEmpty {
|
||||
if oldValue != self.visibility {
|
||||
for (_, itemLayer) in self.inlineStickerItemLayers {
|
||||
let isItemVisible: Bool
|
||||
switch self.visibility {
|
||||
case .none:
|
||||
isItemVisible = false
|
||||
case let .visible(_, subRect):
|
||||
var subRect = subRect
|
||||
subRect.origin.x = 0.0
|
||||
subRect.size.width = 10000.0
|
||||
if itemLayer.frame.intersects(subRect) {
|
||||
isItemVisible = true
|
||||
} else {
|
||||
isItemVisible = false
|
||||
}
|
||||
}
|
||||
itemLayer.isVisibleForAnimations = isItemVisible
|
||||
}
|
||||
if oldValue != self.visibility {
|
||||
switch self.visibility {
|
||||
case .none:
|
||||
self.textNode.visibilityRect = nil
|
||||
case let .visible(_, subRect):
|
||||
var subRect = subRect
|
||||
subRect.origin.x = 0.0
|
||||
subRect.size.width = 10000.0
|
||||
self.textNode.visibilityRect = subRect
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init() {
|
||||
self.textNode = TextNode()
|
||||
self.textNode = TextNodeWithEntities()
|
||||
|
||||
self.statusNode = ChatMessageDateAndStatusNode()
|
||||
|
||||
@ -119,11 +86,11 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
super.init()
|
||||
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.contentMode = .topLeft
|
||||
self.textNode.contentsScale = UIScreenScale
|
||||
self.textNode.displaysAsynchronously = true
|
||||
self.addSubnode(self.textNode)
|
||||
self.textNode.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.textNode.contentMode = .topLeft
|
||||
self.textNode.textNode.contentsScale = UIScreenScale
|
||||
self.textNode.textNode.displaysAsynchronously = true
|
||||
self.addSubnode(self.textNode.textNode)
|
||||
self.addSubnode(self.textAccessibilityOverlayNode)
|
||||
|
||||
self.textAccessibilityOverlayNode.openUrl = { [weak self] url in
|
||||
@ -152,7 +119,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
|
||||
let textLayout = TextNode.asyncLayout(self.textNode)
|
||||
let textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
||||
let spoilerTextLayout = TextNode.asyncLayout(self.spoilerTextNode)
|
||||
let statusLayout = self.statusNode.asyncLayout()
|
||||
|
||||
@ -354,7 +321,6 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil)
|
||||
var updatedAttributes: [NSAttributedString.Key: Any] = currentDict
|
||||
updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor
|
||||
updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId))
|
||||
updatedAttributes[ChatTextInputAttributes.customEmoji] = ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId)
|
||||
|
||||
let insertString = NSAttributedString(string: updatedString.attributedSubstring(from: range).string, attributes: updatedAttributes)
|
||||
@ -445,29 +411,29 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.cachedChatMessageText = updatedCachedChatMessageText
|
||||
}
|
||||
|
||||
let cachedLayout = strongSelf.textNode.cachedLayout
|
||||
let cachedLayout = strongSelf.textNode.textNode.cachedLayout
|
||||
|
||||
if case .System = animation {
|
||||
if let cachedLayout = cachedLayout {
|
||||
if !cachedLayout.areLinesEqual(to: textLayout) {
|
||||
if let textContents = strongSelf.textNode.contents {
|
||||
if let textContents = strongSelf.textNode.textNode.contents {
|
||||
let fadeNode = ASDisplayNode()
|
||||
fadeNode.displaysAsynchronously = false
|
||||
fadeNode.contents = textContents
|
||||
fadeNode.frame = strongSelf.textNode.frame
|
||||
fadeNode.frame = strongSelf.textNode.textNode.frame
|
||||
fadeNode.isLayerBacked = true
|
||||
strongSelf.addSubnode(fadeNode)
|
||||
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
|
||||
fadeNode?.removeFromSupernode()
|
||||
})
|
||||
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = textApply()
|
||||
animation.animator.updateFrame(layer: strongSelf.textNode.layer, frame: textFrame, completion: nil)
|
||||
let _ = textApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: messageTheme.mediaPlaceholderColor))
|
||||
animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil)
|
||||
|
||||
if let (_, spoilerTextApply) = spoilerTextLayoutAndApply {
|
||||
let spoilerTextNode = spoilerTextApply()
|
||||
@ -514,8 +480,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
strongSelf.textAccessibilityOverlayNode.frame = textFrame
|
||||
strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
|
||||
|
||||
strongSelf.updateInlineStickers(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, textLayout: textLayout, placeholderColor: messageTheme.mediaPlaceholderColor)
|
||||
|
||||
|
||||
if let statusSizeAndApply = statusSizeAndApply {
|
||||
animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0), completion: nil)
|
||||
@ -546,73 +511,29 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) {
|
||||
var nextIndexById: [Int64: Int] = [:]
|
||||
var validIds: [InlineStickerItemLayer.Key] = []
|
||||
|
||||
if let textLayout = textLayout {
|
||||
for item in textLayout.embeddedItems {
|
||||
if let stickerItem = item.value as? InlineStickerItem {
|
||||
let index: Int
|
||||
if let currentNext = nextIndexById[stickerItem.emoji.fileId] {
|
||||
index = currentNext
|
||||
} else {
|
||||
index = 0
|
||||
}
|
||||
nextIndexById[stickerItem.emoji.fileId] = index + 1
|
||||
let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index)
|
||||
validIds.append(id)
|
||||
|
||||
let itemLayer: InlineStickerItemLayer
|
||||
if let current = self.inlineStickerItemLayers[id] {
|
||||
itemLayer = current
|
||||
} else {
|
||||
itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, emoji: stickerItem.emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor)
|
||||
self.inlineStickerItemLayers[id] = itemLayer
|
||||
self.textNode.layer.addSublayer(itemLayer)
|
||||
itemLayer.isVisibleForAnimations = self.isVisibleForAnimations
|
||||
}
|
||||
|
||||
itemLayer.frame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 0.0).center, size: CGSize()).insetBy(dx: -12.0, dy: -12.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removeKeys: [InlineStickerItemLayer.Key] = []
|
||||
for (key, itemLayer) in self.inlineStickerItemLayers {
|
||||
if !validIds.contains(key) {
|
||||
removeKeys.append(key)
|
||||
itemLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
for key in removeKeys {
|
||||
self.inlineStickerItemLayers.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.textNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
||||
let textNodeFrame = self.textNode.frame
|
||||
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
||||
let textNodeFrame = self.textNode.textNode.frame
|
||||
if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) {
|
||||
return .none
|
||||
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
var concealed = true
|
||||
if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||
if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
||||
}
|
||||
return .url(url: url, concealed: concealed)
|
||||
@ -669,8 +590,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var rects: [CGRect]?
|
||||
var spoilerRects: [CGRect]?
|
||||
if let point = point {
|
||||
let textNodeFrame = self.textNode.frame
|
||||
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
||||
let textNodeFrame = self.textNode.textNode.frame
|
||||
if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
||||
let possibleNames: [String] = [
|
||||
TelegramTextAttributes.URL,
|
||||
TelegramTextAttributes.PeerMention,
|
||||
@ -682,12 +603,12 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
]
|
||||
for name in possibleNames {
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
|
||||
rects = self.textNode.attributeRects(name: name, at: index)
|
||||
rects = self.textNode.textNode.attributeRects(name: name, at: index)
|
||||
break
|
||||
}
|
||||
}
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)] {
|
||||
spoilerRects = self.textNode.attributeRects(name: TelegramTextAttributes.Spoiler, at: index)
|
||||
spoilerRects = self.textNode.textNode.attributeRects(name: TelegramTextAttributes.Spoiler, at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -701,9 +622,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
} else {
|
||||
linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor)
|
||||
self.linkHighlightingNode = linkHighlightingNode
|
||||
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
|
||||
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode)
|
||||
}
|
||||
linkHighlightingNode.frame = self.textNode.frame
|
||||
linkHighlightingNode.frame = self.textNode.textNode.frame
|
||||
linkHighlightingNode.updateRects(rects)
|
||||
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
||||
self.linkHighlightingNode = nil
|
||||
@ -716,16 +637,16 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? {
|
||||
if let item = self.item {
|
||||
let textNodeFrame = self.textNode.frame
|
||||
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
||||
let textNodeFrame = self.textNode.textNode.frame
|
||||
if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
||||
if let value = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
if let rects = self.textNode.attributeRects(name: TelegramTextAttributes.URL, at: index), !rects.isEmpty {
|
||||
if let rects = self.textNode.textNode.attributeRects(name: TelegramTextAttributes.URL, at: index), !rects.isEmpty {
|
||||
var rect = rects[0]
|
||||
for i in 1 ..< rects.count {
|
||||
rect = rect.union(rects[i])
|
||||
}
|
||||
var concealed = true
|
||||
if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||
if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||
concealed = !doesUrlMatchText(url: value, text: attributeText, fullText: fullText)
|
||||
}
|
||||
return (item.message, .url(self, rect, value, concealed))
|
||||
@ -742,7 +663,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
let rectsSet: [[CGRect]]
|
||||
if let text = text, let messages = messages, !text.isEmpty, messages.contains(item.message.index) {
|
||||
rectsSet = self.textNode.textRangesRects(text: text)
|
||||
rectsSet = self.textNode.textNode.textRangesRects(text: text)
|
||||
} else {
|
||||
rectsSet = []
|
||||
}
|
||||
@ -754,9 +675,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
} else {
|
||||
textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.textHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.textHighlightColor)
|
||||
self.textHighlightingNodes.append(textHighlightNode)
|
||||
self.insertSubnode(textHighlightNode, belowSubnode: self.textNode)
|
||||
self.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode)
|
||||
}
|
||||
textHighlightNode.frame = self.textNode.frame
|
||||
textHighlightNode.frame = self.textNode.textNode.frame
|
||||
textHighlightNode.updateRects(rects)
|
||||
}
|
||||
for i in (rectsSet.count ..< self.textHighlightingNodes.count).reversed() {
|
||||
@ -791,7 +712,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
knobColor = item.presentationData.theme.theme.chat.message.outgoing.textSelectionKnobColor
|
||||
}
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor), strings: item.presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor), strings: item.presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { [weak self] value in
|
||||
self?.updateIsTextSelectionActive?(value)
|
||||
}, present: { [weak self] c, a in
|
||||
self?.item?.controllerInteraction.presentGlobalOverlayController(c, a)
|
||||
@ -802,7 +723,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
item.controllerInteraction.performTextSelectionAction(item.message.stableId, text, action)
|
||||
})
|
||||
textSelectionNode.updateRange = { [weak self] selectionRange in
|
||||
if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange {
|
||||
if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange {
|
||||
for (spoilerRange, _) in textLayout.spoilers {
|
||||
if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 {
|
||||
dustNode.update(revealed: true)
|
||||
@ -813,9 +734,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
self.textSelectionNode = textSelectionNode
|
||||
self.addSubnode(textSelectionNode)
|
||||
self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode)
|
||||
textSelectionNode.frame = self.textNode.frame
|
||||
textSelectionNode.highlightAreaNode.frame = self.textNode.frame
|
||||
self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode.textNode)
|
||||
textSelectionNode.frame = self.textNode.textNode.frame
|
||||
textSelectionNode.highlightAreaNode.frame = self.textNode.textNode.frame
|
||||
}
|
||||
} else {
|
||||
if let textSelectionNode = self.textSelectionNode {
|
||||
@ -851,14 +772,14 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
sourceView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak sourceView] _ in
|
||||
sourceView?.removeFromSuperview()
|
||||
})
|
||||
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.08)
|
||||
self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.08)
|
||||
|
||||
let offset = CGPoint(
|
||||
x: sourceView.frame.minX - (self.textNode.frame.minX - 0.0),
|
||||
y: sourceView.frame.minY - (self.textNode.frame.minY - 3.0) - scrollOffset
|
||||
x: sourceView.frame.minX - (self.textNode.textNode.frame.minX - 0.0),
|
||||
y: sourceView.frame.minY - (self.textNode.textNode.frame.minY - 3.0) - scrollOffset
|
||||
)
|
||||
|
||||
transition.vertical.animatePositionAdditive(node: self.textNode, offset: offset)
|
||||
transition.vertical.animatePositionAdditive(node: self.textNode.textNode, offset: offset)
|
||||
transition.updatePosition(layer: sourceView.layer, position: CGPoint(x: sourceView.layer.position.x - offset.x, y: sourceView.layer.position.y - offset.y))
|
||||
|
||||
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
|
@ -650,10 +650,10 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
if entities.count > 0 {
|
||||
messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||
} else {
|
||||
messageText = NSAttributedString(string: foldLineBreaks(textString), font: textFont, textColor: textColor)
|
||||
messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: textColor)
|
||||
}
|
||||
} else {
|
||||
messageText = NSAttributedString(string: foldLineBreaks(textString), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor)
|
||||
messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor)
|
||||
}
|
||||
|
||||
let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
@ -170,7 +170,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode {
|
||||
if let currentEditMediaReference = self.currentEditMediaReference {
|
||||
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
|
||||
}
|
||||
(text, _, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId)
|
||||
let (attributedText, _, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId)
|
||||
text = attributedText.string
|
||||
}
|
||||
|
||||
var updatedMediaReference: AnyMediaReference?
|
||||
|
@ -14,6 +14,9 @@ import TelegramStringFormatting
|
||||
import InvisibleInkDustNode
|
||||
import TextFormat
|
||||
import ChatPresentationInterfaceState
|
||||
import TextNodeWithEntities
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
|
||||
final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
private let messageDisposable = MetaDisposable()
|
||||
@ -25,7 +28,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
let lineNode: ASImageNode
|
||||
let iconNode: ASImageNode
|
||||
let titleNode: ImmediateTextNode
|
||||
let textNode: ImmediateTextNode
|
||||
let textNode: ImmediateTextNodeWithEntities
|
||||
var dustNode: InvisibleInkDustNode?
|
||||
let imageNode: TransformImageNode
|
||||
|
||||
@ -36,7 +39,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
|
||||
private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)?
|
||||
|
||||
init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) {
|
||||
init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) {
|
||||
self.messageId = messageId
|
||||
|
||||
self.theme = theme
|
||||
@ -63,11 +66,20 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode = ImmediateTextNodeWithEntities()
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
|
||||
|
||||
if let animationCache = animationCache, let animationRenderer = animationRenderer {
|
||||
self.textNode.arguments = TextNodeWithEntities.Arguments(
|
||||
context: context,
|
||||
cache: animationCache,
|
||||
renderer: animationRenderer,
|
||||
placeholderColor: theme.list.mediaPlaceholderColor
|
||||
)
|
||||
}
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
self.imageNode.contentAnimations = [.subsequentUpdates]
|
||||
self.imageNode.isHidden = true
|
||||
@ -119,7 +131,9 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
default:
|
||||
isMedia = true
|
||||
}
|
||||
(text, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId)
|
||||
let (attributedText, _, isTextValue) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId)
|
||||
text = attributedText.string
|
||||
isText = isTextValue
|
||||
} else {
|
||||
isMedia = false
|
||||
}
|
||||
@ -128,9 +142,10 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
let messageText: NSAttributedString
|
||||
if isText, let message = message {
|
||||
let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in
|
||||
if case .Spoiler = entity.type {
|
||||
switch entity.type {
|
||||
case .Spoiler, .CustomEmoji:
|
||||
return true
|
||||
} else {
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -135,14 +135,14 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode {
|
||||
} else if content.type == "telegram_theme" {
|
||||
text = strings.Message_Theme
|
||||
} else {
|
||||
text = stringForMediaKind(mediaKind, strings: self.strings).0
|
||||
text = stringForMediaKind(mediaKind, strings: self.strings).0.string
|
||||
}
|
||||
} else if content.type == "telegram_theme" {
|
||||
text = strings.Message_Theme
|
||||
} else if content.type == "video" {
|
||||
text = stringForMediaKind(.video, strings: self.strings).0
|
||||
text = stringForMediaKind(.video, strings: self.strings).0.string
|
||||
} else if let _ = content.image {
|
||||
text = stringForMediaKind(.image, strings: self.strings).0
|
||||
text = stringForMediaKind(.image, strings: self.strings).0.string
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user