[Temp] custom emoji layouts

This commit is contained in:
Ali 2022-07-08 15:09:55 +02:00
parent 2377d3e08c
commit 1ac654e8b2
26 changed files with 1155 additions and 283 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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()
}

View File

@ -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?

View File

@ -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 {

View File

@ -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",

View File

@ -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())

View File

@ -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(

View File

@ -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

View File

@ -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)

View 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",
],
)

View File

@ -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
}
}
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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?

View File

@ -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
}
}

View File

@ -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
}
}