Blockquote experiments

This commit is contained in:
Isaac 2024-05-23 23:49:43 +04:00
parent cda0334a8b
commit b4dd3591af
28 changed files with 3748 additions and 410 deletions

View File

@ -400,7 +400,7 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
case strikethrough
case underline
case spoiler
case quote
case quote(isCollapsed: Bool)
case codeBlock(language: String?)
public init(from decoder: Decoder) throws {
@ -430,7 +430,7 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
case 8:
self = .spoiler
case 9:
self = .quote
self = .quote(isCollapsed: try container.decodeIfPresent(Bool.self, forKey: "isCollapsed") ?? false)
case 10:
self = .codeBlock(language: try container.decodeIfPresent(String.self, forKey: "l"))
default:
@ -464,8 +464,9 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
try container.encode(7 as Int32, forKey: "t")
case .spoiler:
try container.encode(8 as Int32, forKey: "t")
case .quote:
case let .quote(isCollapsed):
try container.encode(9 as Int32, forKey: "t")
try container.encode(isCollapsed, forKey: "isCollapsed")
case let .codeBlock(language):
try container.encode(10 as Int32, forKey: "t")
try container.encodeIfPresent(language, forKey: "l")
@ -545,7 +546,7 @@ public struct ChatTextInputStateText: Codable, Equatable {
} else if key == ChatTextInputAttributes.block, let value = value as? ChatTextInputTextQuoteAttribute {
switch value.kind {
case .quote:
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .quote, range: range.location ..< (range.location + range.length)))
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .quote(isCollapsed: value.isCollapsed), range: range.location ..< (range.location + range.length)))
case let .code(language):
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .codeBlock(language: language), range: range.location ..< (range.location + range.length)))
}
@ -593,10 +594,10 @@ public struct ChatTextInputStateText: Codable, Equatable {
result.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case .spoiler:
result.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case .quote:
result.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case let .quote(isCollapsed):
result.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: isCollapsed), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case let .codeBlock(language):
result.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: language)), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
result.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: language), isCollapsed: false), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
}
}
return result

View File

@ -569,6 +569,8 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority;
@property (nonatomic) bool disableClearContentsOnHide;
- (void)displayImmediately;
@end
/**

View File

@ -1715,7 +1715,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote)), inputMode)
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false)), inputMode)
}
}
@ -1723,7 +1723,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil))), inputMode)
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil), isCollapsed: false)), inputMode)
}
}

View File

@ -59,7 +59,7 @@ public func chatTextInputAddFormattingAttribute(_ state: ChatTextInputState, att
for (key, value) in attributes {
if let value = value as? ChatTextInputTextQuoteAttribute {
result.removeAttribute(key, range: range)
result.addAttribute(key, value: ChatTextInputTextQuoteAttribute(kind: value.kind), range: range)
result.addAttribute(key, value: ChatTextInputTextQuoteAttribute(kind: value.kind, isCollapsed: value.isCollapsed), range: range)
}
}
}
@ -72,7 +72,7 @@ public func chatTextInputAddFormattingAttribute(_ state: ChatTextInputState, att
if addAttribute {
if attribute == ChatTextInputAttributes.block {
result.addAttribute(attribute, value: value ?? ChatTextInputTextQuoteAttribute(kind: .quote), range: nsRange)
result.addAttribute(attribute, value: value ?? ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false), range: nsRange)
var selectionIndex = nsRange.upperBound
if nsRange.upperBound != result.length && (result.string as NSString).character(at: nsRange.upperBound) != 0x0a {
result.insert(NSAttributedString(string: "\n"), at: nsRange.upperBound)
@ -197,6 +197,6 @@ public func chatTextInputAddQuoteAttribute(_ state: ChatTextInputState, selectio
for (attribute, range) in attributesToRemove {
result.removeAttribute(attribute, range: range)
}
result.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: kind), range: nsRange)
result.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: kind, isCollapsed: false), range: nsRange)
return ChatTextInputState(inputText: result, selectionRange: selectionRange)
}

View File

@ -557,14 +557,14 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe
@objc func formatAttributesQuote(_ sender: Any) {
self.inputMenu.back()
if let item = self.item {
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote))
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false))
}
}
@objc func formatAttributesCodeBlock(_ sender: Any) {
self.inputMenu.back()
if let item = self.item {
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil)))
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil), isCollapsed: false))
}
}

View File

@ -1822,6 +1822,8 @@ public protocol ControlledTransitionAnimator: AnyObject {
func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?)
func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?)
func updateContentsRect(layer: CALayer, contentsRect: CGRect, completion: ((Bool) -> Void)?)
func updateTransform(layer: CALayer, transform: CATransform3D, completion: ((Bool) -> Void)?)
func updateBackgroundColor(layer: CALayer, color: UIColor, completion: ((Bool) -> Void)?)
}
protocol AnyValueProviding {
@ -1967,6 +1969,83 @@ extension CGRect: AnyValueProviding {
}
}
extension CATransform3D: Equatable {
public static func ==(lhs: CATransform3D, rhs: CATransform3D) -> Bool {
return CATransform3DEqualToTransform(lhs, rhs)
}
}
extension CATransform3D: AnyValueProviding {
func interpolate(with other: CATransform3D, fraction: CGFloat) -> CATransform3D {
return CATransform3D(
m11: self.m11.interpolate(with: other.m11, fraction: fraction),
m12: self.m12.interpolate(with: other.m12, fraction: fraction),
m13: self.m13.interpolate(with: other.m13, fraction: fraction),
m14: self.m14.interpolate(with: other.m14, fraction: fraction),
m21: self.m21.interpolate(with: other.m21, fraction: fraction),
m22: self.m22.interpolate(with: other.m22, fraction: fraction),
m23: self.m23.interpolate(with: other.m23, fraction: fraction),
m24: self.m24.interpolate(with: other.m24, fraction: fraction),
m31: self.m31.interpolate(with: other.m31, fraction: fraction),
m32: self.m32.interpolate(with: other.m32, fraction: fraction),
m33: self.m33.interpolate(with: other.m33, fraction: fraction),
m34: self.m34.interpolate(with: other.m34, fraction: fraction),
m41: self.m41.interpolate(with: other.m41, fraction: fraction),
m42: self.m42.interpolate(with: other.m42, fraction: fraction),
m43: self.m43.interpolate(with: other.m43, fraction: fraction),
m44: self.m44.interpolate(with: other.m44, fraction: fraction)
)
}
var anyValue: ControlledTransitionProperty.AnyValue {
return ControlledTransitionProperty.AnyValue(
value: self,
nsValue: NSValue(caTransform3D: self),
stringValue: { "\(self)" },
isEqual: { other in
if let otherValue = other.value as? CATransform3D {
return self == otherValue
} else {
return false
}
},
interpolate: { other, fraction in
guard let otherValue = other.value as? CATransform3D else {
preconditionFailure()
}
return self.interpolate(with: otherValue, fraction: fraction).anyValue
}
)
}
}
extension CGColor: AnyValueProviding {
func interpolate(with other: CGColor, fraction: CGFloat) -> CGColor {
return UIColor(cgColor: self).mixedWith(UIColor(cgColor: other), alpha: fraction).cgColor
}
var anyValue: ControlledTransitionProperty.AnyValue {
return ControlledTransitionProperty.AnyValue(
value: self,
nsValue: self,
stringValue: { "\(self)" },
isEqual: { other in
if CFGetTypeID(other.value as CFTypeRef) == CGColor.typeID {
return self == (other.value as! CGColor)
} else {
return false
}
},
interpolate: { other, fraction in
guard CFGetTypeID(other.value as CFTypeRef) == CGColor.typeID else {
preconditionFailure()
}
return self.interpolate(with: other.value as! CGColor, fraction: fraction).anyValue
}
)
}
}
final class ControlledTransitionProperty {
final class AnyValue: Equatable, CustomStringConvertible {
let value: Any
@ -2232,6 +2311,76 @@ public final class ControlledTransition {
self.updateBounds(layer: layer, bounds: CGRect(origin: CGPoint(), size: frame.size), completion: nil)
}
public func updateTransform(layer: CALayer, transform: CATransform3D, completion: ((Bool) -> Void)?) {
if layer.transform == transform {
return
}
let fromValue: CATransform3D
if let animationKeys = layer.animationKeys(), animationKeys.contains(where: { key in
guard let animation = layer.animation(forKey: key) as? CAPropertyAnimation else {
return false
}
if animation.keyPath == "transform" {
return true
} else {
return false
}
}) {
fromValue = layer.presentation()?.transform ?? layer.transform
} else {
fromValue = layer.transform
}
layer.transform = transform
self.add(animation: ControlledTransitionProperty(
layer: layer,
path: "transform",
fromValue: fromValue,
toValue: transform,
completion: completion
))
}
public func updateBackgroundColor(layer: CALayer, color: UIColor, completion: ((Bool) -> Void)?) {
if let currentColor = layer.backgroundColor, currentColor == color.cgColor {
if let completion = completion {
completion(true)
}
return
}
let fromValue: CGColor?
if let animationKeys = layer.animationKeys(), animationKeys.contains(where: { key in
guard let animation = layer.animation(forKey: key) as? CAPropertyAnimation else {
return false
}
if animation.keyPath == "backgroundColor" {
return true
} else {
return false
}
}) {
fromValue = layer.presentation()?.backgroundColor ?? layer.backgroundColor
} else {
fromValue = layer.backgroundColor
}
var mappedFromValue: UIColor
if let fromValue {
mappedFromValue = UIColor(cgColor: fromValue)
} else {
mappedFromValue = .clear
}
layer.backgroundColor = color.cgColor
self.add(animation: ControlledTransitionProperty(
layer: layer,
path: "backgroundColor",
fromValue: mappedFromValue.cgColor,
toValue: color.cgColor,
completion: completion
))
}
public func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?) {
if layer.cornerRadius == cornerRadius {
return
@ -2305,6 +2454,14 @@ public final class ControlledTransition {
self.transition.updatePosition(layer: layer, position: position, completion: completion)
}
public func updateTransform(layer: CALayer, transform: CATransform3D, completion: ((Bool) -> Void)?) {
self.transition.updateTransform(layer: layer, transform: CATransform3DGetAffineTransform(transform), completion: completion)
}
public func updateBackgroundColor(layer: CALayer, color: UIColor, completion: ((Bool) -> Void)?) {
self.transition.updateBackgroundColor(layer: layer, color: color, completion: completion)
}
public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, completion: ((Bool) -> Void)?) {
self.transition.animatePosition(layer: layer, from: fromValue, to: toValue, completion: completion)
}

View File

@ -8,7 +8,7 @@ open class ASImageNode: ASDisplayNode {
if self.isNodeLoaded {
if let image = self.image {
let capInsets = image.capInsets
if capInsets.left.isZero && capInsets.top.isZero {
if capInsets.left.isZero && capInsets.top.isZero && capInsets.right.isZero && capInsets.bottom.isZero {
self.contentsScale = image.scale
self.contents = image.cgImage
} else {

View File

@ -83,14 +83,16 @@ public final class TextNodeBlockQuoteData: NSObject {
public let secondaryColor: UIColor?
public let tertiaryColor: UIColor?
public let backgroundColor: UIColor
public let isCollapsible: Bool
public init(kind: Kind, title: NSAttributedString?, color: UIColor, secondaryColor: UIColor?, tertiaryColor: UIColor?, backgroundColor: UIColor) {
public init(kind: Kind, title: NSAttributedString?, color: UIColor, secondaryColor: UIColor?, tertiaryColor: UIColor?, backgroundColor: UIColor, isCollapsible: Bool) {
self.kind = kind
self.title = title
self.color = color
self.secondaryColor = secondaryColor
self.tertiaryColor = tertiaryColor
self.backgroundColor = backgroundColor
self.isCollapsible = isCollapsible
super.init()
}
@ -1167,7 +1169,13 @@ private func addAttachment(attachment: UIImage, line: TextNodeLine, ascent: CGFl
line.attachments.append(TextNodeAttachment(range: NSMakeRange(startIndex, endIndex - startIndex), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), attachment: attachment))
}
open class TextNode: ASDisplayNode {
public protocol TextNodeProtocol: ASDisplayNode {
var currentText: NSAttributedString? { get }
func textRangeRects(in range: NSRange) -> (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)?
func attributesAtPoint(_ point: CGPoint, orNearest: Bool) -> (Int, [NSAttributedString.Key: Any])?
}
open class TextNode: ASDisplayNode, TextNodeProtocol {
public struct RenderContentTypes: OptionSet {
public var rawValue: Int
@ -1196,6 +1204,14 @@ open class TextNode: ASDisplayNode {
public internal(set) var cachedLayout: TextNodeLayout?
public var renderContentTypes: RenderContentTypes = .all
public var currentText: NSAttributedString? {
return self.cachedLayout?.attributedString
}
public func textRangeRects(in range: NSRange) -> (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)? {
return self.cachedLayout?.rangeRects(in: range)
}
override public init() {
super.init()

View File

@ -588,7 +588,7 @@ bool MTCheckIsSafePrime(id<EncryptionProvider> provider, NSData *numberBytes, id
id<MTBignum> bnNumber = [context create];
[context assignBinTo:bnNumber value:numberBytes];
int result = [context isPrime:bnNumber numberOfChecks:30];
int result = [context isPrime:bnNumber numberOfChecks:64];
if (result == 1) {
id<MTBignum> bnNumberOne = [context create];
@ -600,7 +600,7 @@ bool MTCheckIsSafePrime(id<EncryptionProvider> provider, NSData *numberBytes, id
id<MTBignum> bnNumberMinusOneDivByTwo = [context create];
[context rightShift1Bit:bnNumberMinusOneDivByTwo a:bnNumberMinusOne];
result = [context isPrime:bnNumberMinusOneDivByTwo numberOfChecks:30];
result = [context isPrime:bnNumberMinusOneDivByTwo numberOfChecks:64];
}
[keychain setObject:@(result == 1) forKey:primeKey group:@"primes"];

View File

@ -277,6 +277,8 @@ private func filterMessageAttributesForForwardedMessage(_ attributes: [MessageAt
return true
case _ as MediaSpoilerMessageAttribute:
return true
case _ as InvertMediaMessageAttribute:
return true
case let attribute as ReplyMessageAttribute:
if attribute.quote != nil {
return true

View File

@ -46,6 +46,10 @@
return self;
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return false;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return true;
}

View File

@ -37,6 +37,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/MessageQuoteComponent",
"//submodules/TelegramUI/Components/TextLoadingEffect",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/InteractiveTextComponent",
],
visibility = [
"//visibility:public",

View File

@ -26,6 +26,7 @@ import ShimmeringLinkNode
import ChatMessageItemCommon
import TextLoadingEffect
import ChatControllerInteraction
import InteractiveTextComponent
private final class CachedChatMessageText {
let text: String
@ -83,8 +84,7 @@ private func findQuoteRange(string: String, quoteText: String, offset: Int?) ->
public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private let containerNode: ASDisplayNode
private let textNode: TextNodeWithEntities
private var spoilerTextNode: TextNodeWithEntities?
private let textNode: InteractiveTextNodeWithEntities
private var dustNode: InvisibleInkDustNode?
private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode
@ -111,19 +111,19 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private var codeHighlightState: (id: EngineMessage.Id, specs: [CachedMessageSyntaxHighlight.Spec], disposable: Disposable)?
private var collapsedBlockIds: Set<Int> = Set()
override public var visibility: ListViewItemNodeVisibility {
didSet {
if oldValue != self.visibility {
switch self.visibility {
case .none:
self.textNode.visibilityRect = nil
self.spoilerTextNode?.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
self.textNode.visibilityRect = subRect
self.spoilerTextNode?.visibilityRect = subRect
}
}
}
@ -132,7 +132,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
required public init() {
self.containerNode = ASDisplayNode()
self.textNode = TextNodeWithEntities()
self.textNode = InteractiveTextNodeWithEntities()
self.textAccessibilityOverlayNode = TextAccessibilityOverlayNode()
@ -140,16 +140,28 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.containerNode)
self.textNode.textNode.isUserInteractionEnabled = false
self.textNode.textNode.isUserInteractionEnabled = true
self.textNode.textNode.contentMode = .topLeft
self.textNode.textNode.contentsScale = UIScreenScale
self.textNode.textNode.displaysAsynchronously = true
//self.containerNode.addSubnode(self.textAccessibilityOverlayNode)
self.containerNode.addSubnode(self.textNode.textNode)
self.containerNode.addSubnode(self.textAccessibilityOverlayNode)
self.textAccessibilityOverlayNode.openUrl = { [weak self] url in
self?.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: false, external: false))
}
self.textNode.textNode.requestToggleBlockCollapsed = { [weak self] blockId in
guard let self, let item = self.item else {
return
}
if self.collapsedBlockIds.contains(blockId) {
self.collapsedBlockIds.remove(blockId)
} else {
self.collapsedBlockIds.insert(blockId)
}
item.controllerInteraction.requestMessageUpdate(item.message.id, false)
}
}
required public init?(coder aDecoder: NSCoder) {
@ -163,11 +175,11 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let spoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode)
let textLayout = InteractiveTextNodeWithEntities.asyncLayout(self.textNode)
let statusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode)
let currentCachedChatMessageText = self.cachedChatMessageText
let collapsedBlockIds = self.collapsedBlockIds
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
@ -503,7 +515,6 @@ public 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[ChatTextInputAttributes.customEmoji] = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile)
let insertString = NSAttributedString(string: updatedString.attributedSubstring(from: range).string, attributes: updatedAttributes)
@ -542,14 +553,20 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0)
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: maximumNumberOfLines, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor, customTruncationToken: customTruncationToken))
let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
if !textLayout.spoilers.isEmpty {
spoilerTextLayoutAndApply = spoilerTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: maximumNumberOfLines, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor, displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true))
} else {
spoilerTextLayoutAndApply = nil
}
let (textLayout, textApply) = textLayout(InteractiveTextNodeLayoutArguments(
attributedString: attributedText,
backgroundColor: nil,
maximumNumberOfLines: maximumNumberOfLines,
truncationType: .end,
constrainedSize: textConstrainedSize,
alignment: .natural,
cutout: nil,
insets: textInsets,
lineColor: messageTheme.accentControlColor,
displayContentsUnderSpoilers: false,
customTruncationToken: customTruncationToken,
collapsedBlocks: collapsedBlockIds
))
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))?
if let statusType = statusType {
@ -559,7 +576,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
let trailingWidthToMeasure: CGFloat
if textLayout.hasRTL {
if let lastSegment = textLayout.segments.last, lastSegment.hasRTL {
trailingWidthToMeasure = 10000.0
} else {
trailingWidthToMeasure = textLayout.trailingLineWidth
@ -630,42 +647,20 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: boundingSize)
let cachedLayout = strongSelf.textNode.textNode.cachedLayout
if case .System = animation {
if let cachedLayout = cachedLayout {
if !cachedLayout.areLinesEqual(to: textLayout) {
if let textContents = strongSelf.textNode.textNode.contents {
let fadeNode = ASDisplayNode()
fadeNode.displaysAsynchronously = false
fadeNode.contents = textContents
fadeNode.frame = strongSelf.textNode.textNode.frame
fadeNode.isLayerBacked = true
strongSelf.containerNode.addSubnode(fadeNode)
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
fadeNode?.removeFromSupernode()
})
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
}
}
}
let _ = textApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: messageTheme.mediaPlaceholderColor, attemptSynchronous: synchronousLoads))
let _ = textApply(InteractiveTextNodeWithEntities.Arguments(
context: item.context,
cache: item.controllerInteraction.presentationContext.animationCache,
renderer: item.controllerInteraction.presentationContext.animationRenderer,
placeholderColor: messageTheme.mediaPlaceholderColor,
attemptSynchronous: synchronousLoads,
textColor: messageTheme.primaryTextColor,
spoilerEffectColor: messageTheme.secondaryTextColor,
animation: animation
))
animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil)
if let (_, spoilerTextApply) = spoilerTextLayoutAndApply {
let spoilerTextNode = spoilerTextApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: messageTheme.mediaPlaceholderColor, attemptSynchronous: synchronousLoads))
if strongSelf.spoilerTextNode == nil {
spoilerTextNode.textNode.alpha = 0.0
spoilerTextNode.textNode.isUserInteractionEnabled = false
spoilerTextNode.textNode.contentMode = .topLeft
spoilerTextNode.textNode.contentsScale = UIScreenScale
spoilerTextNode.textNode.displaysAsynchronously = false
strongSelf.containerNode.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textAccessibilityOverlayNode)
strongSelf.spoilerTextNode = spoilerTextNode
}
/*if let (_, spoilerTextApply) = spoilerTextLayoutAndApply {
let spoilerTextNode = spoilerTextApply(InteractiveTextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: messageTheme.mediaPlaceholderColor, attemptSynchronous: synchronousLoads, animation: animation))
strongSelf.spoilerTextNode?.textNode.frame = textFrame
@ -687,18 +682,16 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.dustNode = nil
dustNode.removeFromSupernode()
}
}
}*/
switch strongSelf.visibility {
case .none:
strongSelf.textNode.visibilityRect = nil
strongSelf.spoilerTextNode?.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
strongSelf.textNode.visibilityRect = subRect
strongSelf.spoilerTextNode?.visibilityRect = subRect
}
if let textSelectionNode = strongSelf.textSelectionNode {
@ -710,7 +703,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
strongSelf.textAccessibilityOverlayNode.frame = textFrame
strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
//TODO:localize
//strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
strongSelf.updateIsTranslating(isTranslating)
@ -852,7 +846,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
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 textLocalPoint = CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)
if let (index, attributes) = self.textNode.textNode.attributesAtPoint(textLocalPoint) {
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) {
return ChatMessageBubbleContentTapAction(content: .none)
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
@ -945,10 +940,14 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
} else if let code = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Code)] as? String {
return ChatMessageBubbleContentTapAction(content: .copy(code))
} else if let _ = attributes[NSAttributedString.Key(rawValue: "Attribute__Blockquote")] {
if let text = self.textNode.textNode.attributeSubstring(name: "Attribute__Blockquote", index: index) {
return ChatMessageBubbleContentTapAction(content: .copy(text.1))
} else {
if let _ = self.textNode.textNode.collapsibleBlockAtPoint(textLocalPoint) {
return ChatMessageBubbleContentTapAction(content: .none)
} else {
if let text = self.textNode.textNode.attributeSubstring(name: "Attribute__Blockquote", index: index) {
return ChatMessageBubbleContentTapAction(content: .copy(text.1))
} else {
return ChatMessageBubbleContentTapAction(content: .none)
}
}
} else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file {
return ChatMessageBubbleContentTapAction(content: .customEmoji(file))
@ -1074,7 +1073,17 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
rectsSet = []
}
for i in 0 ..< rectsSet.count {
let rects = rectsSet[i]
var rects = rectsSet[i]
if rects.count > 1 {
for i in 0 ..< rects.count - 1 {
let deltaY = rects[i + 1].minY - rects[i].maxY
if deltaY > 0.0 && deltaY <= 2.0 {
rects[i].size.height += deltaY * 0.5
rects[i + 1].size.height += deltaY * 0.5
rects[i + 1].origin.y -= deltaY * 0.5
}
}
}
let textHighlightNode: LinkHighlightingNode
if i < self.textHighlightingNodes.count {
textHighlightNode = self.textHighlightingNodes[i]
@ -1302,11 +1311,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
guard let strongSelf = self else {
return
}
if 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)
return
if let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, textLayout.segments.contains(where: { !$0.spoilers.isEmpty }), let selectionRange {
for segment in textLayout.segments {
for (spoilerRange, _) in segment.spoilers {
if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 {
dustNode.update(revealed: true)
return
}
}
}
}

View File

@ -0,0 +1,29 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "InteractiveTextComponent",
module_name = "InteractiveTextComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/AppBundle",
"//submodules/TextFormat",
"//submodules/AccountContext",
"//submodules/TelegramUI/Components/AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/TelegramCore",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView",
"//submodules/InvisibleInkDustNode",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,366 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import CoreText
import AppBundle
import ComponentFlow
import TextFormat
import AccountContext
import AnimationCache
import MultiAnimationRenderer
import TelegramCore
import EmojiTextAttachmentView
import InvisibleInkDustNode
private final class InlineStickerItem: Hashable {
let emoji: ChatTextInputTextCustomEmojiAttribute
let file: TelegramMediaFile?
let fontSize: CGFloat
init(emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, fontSize: CGFloat) {
self.emoji = emoji
self.file = file
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.file?.fileId != rhs.file?.fileId {
return false
}
if lhs.fontSize != rhs.fontSize {
return false
}
return true
}
}
private final class RunDelegateData {
let ascent: CGFloat
let descent: CGFloat
let width: CGFloat
init(ascent: CGFloat, descent: CGFloat, width: CGFloat) {
self.ascent = ascent
self.descent = descent
self.width = width
}
}
public final class InteractiveTextNodeWithEntities {
public final class Arguments {
public let context: AccountContext
public let cache: AnimationCache
public let renderer: MultiAnimationRenderer
public let placeholderColor: UIColor
public let attemptSynchronous: Bool
public let textColor: UIColor
public let spoilerEffectColor: UIColor
public let animation: ListViewItemUpdateAnimation
public init(
context: AccountContext,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
placeholderColor: UIColor,
attemptSynchronous: Bool,
textColor: UIColor,
spoilerEffectColor: UIColor,
animation: ListViewItemUpdateAnimation
) {
self.context = context
self.cache = cache
self.renderer = renderer
self.placeholderColor = placeholderColor
self.attemptSynchronous = attemptSynchronous
self.textColor = textColor
self.spoilerEffectColor = spoilerEffectColor
self.animation = animation
}
public func withUpdatedPlaceholderColor(_ color: UIColor) -> Arguments {
return Arguments(
context: self.context,
cache: self.cache,
renderer: self.renderer,
placeholderColor: color,
attemptSynchronous: self.attemptSynchronous,
textColor: self.textColor,
spoilerEffectColor: self.spoilerEffectColor,
animation: self.animation
)
}
}
private final class InlineStickerItemLayerData {
let itemLayer: InlineStickerItemLayer
var rect: CGRect = CGRect()
init(itemLayer: InlineStickerItemLayer) {
self.itemLayer = itemLayer
}
}
public let textNode: InteractiveTextNode
private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayerData] = [:]
private var dustEffectNodes: [Int: InvisibleInkDustNode] = [:]
private var enableLooping: Bool = true
public var visibilityRect: CGRect? {
didSet {
if !self.inlineStickerItemLayers.isEmpty && oldValue != self.visibilityRect {
for (_, itemLayerData) in self.inlineStickerItemLayers {
let isItemVisible: Bool
if let visibilityRect = self.visibilityRect {
if itemLayerData.rect.intersects(visibilityRect) {
isItemVisible = true
} else {
isItemVisible = false
}
} else {
isItemVisible = false
}
itemLayerData.itemLayer.isVisibleForAnimations = self.enableLooping && isItemVisible
}
}
}
}
public init() {
self.textNode = InteractiveTextNode()
}
private init(textNode: InteractiveTextNode) {
self.textNode = textNode
}
public static func asyncLayout(_ maybeNode: InteractiveTextNodeWithEntities?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (InteractiveTextNodeWithEntities.Arguments?) -> InteractiveTextNodeWithEntities) {
let makeLayout = InteractiveTextNode.asyncLayout(maybeNode?.textNode)
return { [weak maybeNode] arguments in
var updatedString: NSAttributedString?
if let sourceString = arguments.attributedString {
let string = NSMutableAttributedString(attributedString: sourceString)
var fullRange = NSRange(location: 0, length: string.length)
var originalTextId = 0
while true {
var found = false
string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, stop in
if let value = value as? ChatTextInputTextCustomEmojiAttribute, let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont {
let updatedSubstring = NSMutableAttributedString(string: "&")
let replacementRange = NSRange(location: 0, length: updatedSubstring.length)
updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange)
updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: replacementRange)
updatedSubstring.addAttribute(originalTextAttributeKey, value: OriginalTextAttribute(id: originalTextId, string: string.attributedSubstring(from: range).string), range: replacementRange)
originalTextId += 1
let itemSize = (font.pointSize * 24.0 / 17.0)
let runDelegateData = RunDelegateData(
ascent: font.ascender,
descent: font.descender,
width: itemSize
)
var callbacks = CTRunDelegateCallbacks(
version: kCTRunDelegateCurrentVersion,
dealloc: { dataRef in
Unmanaged<RunDelegateData>.fromOpaque(dataRef).release()
},
getAscent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().ascent
},
getDescent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().descent
},
getWidth: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().width
}
)
if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) {
updatedSubstring.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: replacementRange)
}
string.replaceCharacters(in: range, with: updatedSubstring)
let updatedRange = NSRange(location: range.location, length: updatedSubstring.length)
found = true
stop.pointee = ObjCBool(true)
fullRange = NSRange(location: updatedRange.upperBound, length: fullRange.upperBound - range.upperBound)
}
})
if !found {
break
}
}
updatedString = string
}
let (layout, apply) = makeLayout(arguments.withAttributedString(updatedString))
return (layout, { applyArguments in
let animation: ListViewItemUpdateAnimation = applyArguments?.animation ?? .None
let result = apply(animation)
if let maybeNode = maybeNode {
if let applyArguments = applyArguments {
maybeNode.updateInteractiveContents(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false, textColor: applyArguments.textColor, spoilerEffectColor: applyArguments.spoilerEffectColor, animation: animation)
}
return maybeNode
} else {
let resultNode = InteractiveTextNodeWithEntities(textNode: result)
if let applyArguments = applyArguments {
resultNode.updateInteractiveContents(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false, textColor: applyArguments.textColor, spoilerEffectColor: applyArguments.spoilerEffectColor, animation: .None)
}
return resultNode
}
})
}
}
private func isItemVisible(itemRect: CGRect) -> Bool {
if let visibilityRect = self.visibilityRect {
return itemRect.intersects(visibilityRect)
} else {
return false
}
}
private func updateInteractiveContents(
context: AccountContext,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
textLayout: InteractiveTextNodeLayout?,
placeholderColor: UIColor,
attemptSynchronousLoad: Bool,
textColor: UIColor,
spoilerEffectColor: UIColor,
animation: ListViewItemUpdateAnimation
) {
self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji
var nextIndexById: [Int64: Int] = [:]
var validIds: [InlineStickerItemLayer.Key] = []
var validDustEffectIds: [Int] = []
if let textLayout {
for i in 0 ..< textLayout.segments.count {
let segment = textLayout.segments[i]
guard let segmentLayer = self.textNode.segmentLayer(index: i), let segmentItem = segmentLayer.item else {
continue
}
for item in segment.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 = floorToScreenPixels(stickerItem.fontSize * 24.0 / 17.0)
var itemFrame = CGRect(origin: item.rect.center, size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0)
itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x)
itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y)
itemFrame.origin.x += segmentItem.contentOffset.x
itemFrame.origin.y += segmentItem.contentOffset.y
let itemLayerData: InlineStickerItemLayerData
if let current = self.inlineStickerItemLayers[id] {
itemLayerData = current
itemLayerData.itemLayer.dynamicColor = item.textColor
if itemLayerData.itemLayer.superlayer !== segmentLayer.renderNode.layer {
segmentLayer.addSublayer(itemLayerData.itemLayer)
}
} else {
let pointSize = floor(itemSize * 1.3)
itemLayerData = InlineStickerItemLayerData(itemLayer: InlineStickerItemLayer(context: context, userLocation: .other, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: item.textColor))
self.inlineStickerItemLayers[id] = itemLayerData
segmentLayer.renderNode.layer.addSublayer(itemLayerData.itemLayer)
itemLayerData.itemLayer.isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame.offsetBy(dx: -segmentItem.contentOffset.x, dy: -segmentItem.contentOffset.x))
}
itemLayerData.itemLayer.opacity = item.isHiddenBySpoiler ? 0.0 : 1.0
itemLayerData.itemLayer.frame = itemFrame
itemLayerData.rect = itemFrame.offsetBy(dx: -segmentItem.contentOffset.x, dy: -segmentItem.contentOffset.y)
}
}
if !segment.spoilers.isEmpty {
validDustEffectIds.append(i)
let dustEffectNode: InvisibleInkDustNode
if let current = self.dustEffectNodes[i] {
dustEffectNode = current
if dustEffectNode.layer.superlayer !== segmentLayer.renderNode.layer {
segmentLayer.renderNode.layer.addSublayer(dustEffectNode.layer)
}
} else {
dustEffectNode = InvisibleInkDustNode(textNode: nil, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency)
self.dustEffectNodes[i] = dustEffectNode
segmentLayer.renderNode.layer.addSublayer(dustEffectNode.layer)
}
let dustNodeFrame = CGRect(origin: CGPoint(), size: segmentItem.size).insetBy(dx: -3.0, dy: -3.0)
dustEffectNode.frame = dustNodeFrame
dustEffectNode.update(
size: dustNodeFrame.size,
color: spoilerEffectColor,
textColor: textColor,
rects: segment.spoilers.map { $0.1.offsetBy(dx: 3.0 + segmentItem.contentOffset.x, dy: segmentItem.contentOffset.y + 3.0).insetBy(dx: 1.0, dy: 1.0) },
wordRects: segment.spoilerWords.map { $0.1.offsetBy(dx: segmentItem.contentOffset.x + 3.0, dy: segmentItem.contentOffset.y + 3.0).insetBy(dx: 1.0, dy: 1.0) }
)
}
}
}
var removeKeys: [InlineStickerItemLayer.Key] = []
for (key, itemLayerData) in self.inlineStickerItemLayers {
if !validIds.contains(key) {
removeKeys.append(key)
itemLayerData.itemLayer.removeFromSuperlayer()
}
}
for key in removeKeys {
self.inlineStickerItemLayers.removeValue(forKey: key)
}
var removeDustEffectIds: [Int] = []
for (id, dustEffectNode) in self.dustEffectNodes {
if !validDustEffectIds.contains(id) {
removeDustEffectIds.append(id)
dustEffectNode.removeFromSupernode()
}
}
for id in removeDustEffectIds {
self.dustEffectNodes.removeValue(forKey: id)
}
}
}

View File

@ -622,7 +622,7 @@ public final class TextFieldComponent: Component {
}
})
self.toggleAttribute(key: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote))
self.toggleAttribute(key: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false))
self.updateSpoilersRevealed(animated: animated)
}
@ -639,7 +639,7 @@ public final class TextFieldComponent: Component {
}
})
self.toggleAttribute(key: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil)))
self.toggleAttribute(key: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil), isCollapsed: false))
self.updateSpoilersRevealed(animated: animated)
}
@ -1438,7 +1438,7 @@ extension TextFieldComponent.InputState {
for (key, value) in attributes {
if let value = value as? ChatTextInputTextQuoteAttribute {
result.removeAttribute(key, range: range)
result.addAttribute(key, value: ChatTextInputTextQuoteAttribute(kind: value.kind), range: range)
result.addAttribute(key, value: ChatTextInputTextQuoteAttribute(kind: value.kind, isCollapsed: value.isCollapsed), range: range)
}
}
}
@ -1451,7 +1451,7 @@ extension TextFieldComponent.InputState {
if addAttribute {
if attribute == ChatTextInputAttributes.block {
result.addAttribute(attribute, value: value ?? ChatTextInputTextQuoteAttribute(kind: .quote), range: nsRange)
result.addAttribute(attribute, value: value ?? ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false), range: nsRange)
var selectionIndex = nsRange.upperBound
if nsRange.upperBound != result.length && (result.string as NSString).character(at: nsRange.upperBound) != 0x0a {
result.insert(NSAttributedString(string: "\n"), at: nsRange.upperBound)

View File

@ -112,12 +112,10 @@ public final class TextLoadingEffectView: UIView {
self.borderBackgroundView.layer.add(animation, forKey: "shimmer")
}
public func update(color: UIColor, textNode: TextNode, range: NSRange) {
public func update(color: UIColor, textNode: TextNodeProtocol, range: NSRange) {
var rectsSet: [CGRect] = []
if let cachedLayout = textNode.cachedLayout {
if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty {
rectsSet = rects
}
if let rects = textNode.textRangeRects(in: range)?.rects, !rects.isEmpty {
rectsSet = rects
}
let maskFrame = CGRect(origin: CGPoint(), size: textNode.bounds.size).insetBy(dx: -4.0, dy: -4.0)

View File

@ -602,10 +602,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
var historyNodeRotated = true
var isChatPreview = false
switch chatPresentationInterfaceState.mode {
case let .standard(standardMode):
if case .embedded(true) = standardMode {
historyNodeRotated = false
} else if case .previewing = standardMode {
isChatPreview = true
}
default:
break
@ -614,7 +617,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.controllerInteraction.chatIsRotated = historyNodeRotated
var getMessageTransitionNode: (() -> ChatMessageTransitionNodeImpl?)?
self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: controller?.updatedPresentationData ?? (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tag: nil, source: source, subject: subject, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), rotated: historyNodeRotated, messageTransitionNode: {
self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: controller?.updatedPresentationData ?? (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tag: nil, source: source, subject: subject, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), rotated: historyNodeRotated, isChatPreview: isChatPreview, messageTransitionNode: {
return getMessageTransitionNode?()
})

View File

@ -718,7 +718,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
return self._isReady.get()
}
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>), chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tag: HistoryViewInputTag?, source: ChatHistoryListSource, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal<Set<MessageId>?, NoError>, mode: ChatHistoryListMode = .bubbles, rotated: Bool = false, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl?) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>), chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tag: HistoryViewInputTag?, source: ChatHistoryListSource, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal<Set<MessageId>?, NoError>, mode: ChatHistoryListMode = .bubbles, rotated: Bool = false, isChatPreview: Bool, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl?) {
var tag = tag
if case .pinnedMessages = subject {
tag = .tag(.pinned)
@ -752,13 +752,15 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
self.prefetchManager = InChatPrefetchManager(context: context)
var displayAdPeer: PeerId?
switch subject {
case .none, .message:
if case let .peer(peerId) = chatLocation {
displayAdPeer = peerId
if !isChatPreview {
switch subject {
case .none, .message:
if case let .peer(peerId) = chatLocation {
displayAdPeer = peerId
}
default:
break
}
default:
break
}
var adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError>
if case .bubbles = mode, let peerId = displayAdPeer {

View File

@ -1200,6 +1200,24 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
self.textInputBackgroundNode.isUserInteractionEnabled = !textInputNode.isUserInteractionEnabled
//self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0])
textInputNode.textView.toggleQuoteCollapse = { [weak self] range in
guard let self else {
return
}
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
let result = NSMutableAttributedString(attributedString: current.inputText)
if let current = result.attribute(ChatTextInputAttributes.block, at: range.lowerBound, effectiveRange: nil) as? ChatTextInputTextQuoteAttribute {
result.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: current.kind, isCollapsed: !current.isCollapsed), range: range)
}
return (ChatTextInputState(
inputText: result,
selectionRange: current.selectionRange
), inputMode)
}
}
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in
if let strongSelf = self {
@ -4250,7 +4268,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote)), inputMode)
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false)), inputMode)
}
}
@ -4258,7 +4276,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil))), inputMode)
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil), isCollapsed: false)), inputMode)
}
}

View File

@ -224,7 +224,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
self.isGlobalSearch = false
}
self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tag: .tag(tagMask), source: source, subject: .message(id: .id(initialMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), messageTransitionNode: { return nil })
self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tag: .tag(tagMask), source: source, subject: .message(id: .id(initialMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), isChatPreview: false, messageTransitionNode: { return nil })
self.historyNode.clipsToBounds = true
super.init()
@ -566,7 +566,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
let historyNode = ChatHistoryListNodeImpl(context: self.context, updatedPresentationData: (self.context.sharedContext.currentPresentationData.with({ $0 }), self.context.sharedContext.presentationData), chatLocation: self.chatLocation, chatLocationContextHolder: chatLocationContextHolder, tag: .tag(tagMask), source: .default, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), messageTransitionNode: { return nil })
let historyNode = ChatHistoryListNodeImpl(context: self.context, updatedPresentationData: (self.context.sharedContext.currentPresentationData.with({ $0 }), self.context.sharedContext.presentationData), chatLocation: self.chatLocation, chatLocationContextHolder: chatLocationContextHolder, tag: .tag(tagMask), source: .default, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), isChatPreview: false, messageTransitionNode: { return nil })
historyNode.clipsToBounds = true
historyNode.preloadPages = true
historyNode.stackFromBottom = true

View File

@ -1638,6 +1638,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
controllerInteraction: controllerInteraction as! ChatControllerInteraction,
selectedMessages: selectedMessages,
mode: mode,
isChatPreview: false,
messageTransitionNode: { return nil }
)
}

View File

@ -249,9 +249,11 @@ public final class ChatTextInputTextQuoteAttribute: NSObject {
}
public let kind: Kind
public let isCollapsed: Bool
public init(kind: Kind) {
public init(kind: Kind, isCollapsed: Bool) {
self.kind = kind
self.isCollapsed = isCollapsed
super.init()
}
@ -264,6 +266,9 @@ public final class ChatTextInputTextQuoteAttribute: NSObject {
if self.kind != other.kind {
return false
}
if self.isCollapsed != other.isCollapsed {
return false
}
return true
}
@ -646,7 +651,7 @@ private func refreshBlockQuotes(text: NSString, initialAttributedText: NSAttribu
if !quoteRangesEqual(quoteRanges, initialQuoteRanges) {
attributedText.removeAttribute(ChatTextInputAttributes.block, range: fullRange)
for (range, attribute) in quoteRanges {
attributedText.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: attribute.kind), range: range)
attributedText.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: attribute.kind, isCollapsed: attribute.isCollapsed), range: range)
}
}
}
@ -1067,7 +1072,7 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu
substring = substring.substring(with: NSRange(location: 0, length: substring.length - 1)) as NSString
}
result.append(NSAttributedString(string: substring as String, attributes: [ChatTextInputAttributes.block: ChatTextInputTextQuoteAttribute(kind: .code(language: language))]))
result.append(NSAttributedString(string: substring as String, attributes: [ChatTextInputAttributes.block: ChatTextInputTextQuoteAttribute(kind: .code(language: language), isCollapsed: false)]))
offsetRanges.append((NSMakeRange(matchIndex + match.range(at: 1).length, text.count), 6))
}
}

View File

@ -171,7 +171,7 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate
} else if key == ChatTextInputAttributes.block, let value = value as? ChatTextInputTextQuoteAttribute {
switch value.kind {
case .quote:
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .BlockQuote(isCollapsed: false)))
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .BlockQuote(isCollapsed: value.isCollapsed)))
case let .code(language):
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Pre(language: language)))
}

View File

@ -49,9 +49,9 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
case let .CustomEmoji(_, fileId):
string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: range)
case let .Pre(language):
string.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: language)), range: range)
case .BlockQuote:
string.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote), range: range)
string.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: language), isCollapsed: false), range: range)
case let .BlockQuote(isCollapsed):
string.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: isCollapsed), range: range)
default:
break
}
@ -219,12 +219,12 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
if let language, !language.isEmpty {
title = NSAttributedString(string: language.capitalized, font: boldFont.withSize(round(boldFont.pointSize * 0.8235294117647058)), textColor: codeBlockTitleColor)
}
string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(kind: .code(language: language), title: title, color: codeBlockAccentColor, secondaryColor: nil, tertiaryColor: nil, backgroundColor: codeBlockBackgroundColor), range: range)
string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(kind: .code(language: language), title: title, color: codeBlockAccentColor, secondaryColor: nil, tertiaryColor: nil, backgroundColor: codeBlockBackgroundColor, isCollapsible: false), range: range)
}
case .BlockQuote:
case let .BlockQuote(isCollapsed):
addFontAttributes(range, .blockQuote)
string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(kind: .quote, title: nil, color: baseQuoteTintColor, secondaryColor: baseQuoteSecondaryTintColor, tertiaryColor: baseQuoteTertiaryTintColor, backgroundColor: baseQuoteTintColor.withMultipliedAlpha(0.1)), range: range)
string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(kind: .quote, title: nil, color: baseQuoteTintColor, secondaryColor: baseQuoteSecondaryTintColor, tertiaryColor: baseQuoteTertiaryTintColor, backgroundColor: baseQuoteTintColor.withMultipliedAlpha(0.1), isCollapsible: isCollapsed), range: range)
case .BankCard:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {

View File

@ -218,7 +218,7 @@ public enum TextSelectionAction: Equatable {
public final class TextSelectionNode: ASDisplayNode {
private let theme: TextSelectionTheme
private let strings: PresentationStrings
private let textNode: TextNode
private let textNode: TextNodeProtocol
private let updateIsActive: (Bool) -> Void
public var canBeginSelection: (CGPoint) -> Bool = { _ in true }
public var updateRange: ((NSRange?) -> Void)?
@ -252,7 +252,7 @@ public final class TextSelectionNode: ASDisplayNode {
private weak var contextMenu: ContextMenuController?
public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNode, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: @escaping () -> ASDisplayNode?, externalKnobSurface: UIView? = nil, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) {
public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNodeProtocol, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: @escaping () -> ASDisplayNode?, externalKnobSurface: UIView? = nil, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) {
self.theme = theme
self.strings = strings
self.textNode = textNode
@ -302,7 +302,7 @@ public final class TextSelectionNode: ASDisplayNode {
return self?.knobAtPoint(point)
}
recognizer.moveKnob = { [weak self] knob, point in
guard let strongSelf = self, let cachedLayout = strongSelf.textNode.cachedLayout, let _ = cachedLayout.attributedString, let currentRange = strongSelf.currentRange else {
guard let strongSelf = self, let currentRange = strongSelf.currentRange else {
return
}
@ -335,7 +335,7 @@ public final class TextSelectionNode: ASDisplayNode {
strongSelf.displayMenu()
}
recognizer.beginSelection = { [weak self] point in
guard let strongSelf = self, let cachedLayout = strongSelf.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
guard let strongSelf = self, let attributedString = strongSelf.textNode.currentText else {
return
}
@ -398,7 +398,7 @@ public final class TextSelectionNode: ASDisplayNode {
}
public func pretendInitiateSelection() {
guard let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
guard let attributedString = self.textNode.currentText else {
return
}
@ -432,7 +432,7 @@ public final class TextSelectionNode: ASDisplayNode {
}
public func pretendExtendSelection(to index: Int) {
guard let cachedLayout = self.textNode.cachedLayout, let _ = cachedLayout.attributedString, let endRangeRect = cachedLayout.rangeRects(in: NSRange(location: index, length: 1))?.rects.first else {
guard let endRangeRect = self.textNode.textRangeRects(in: NSRange(location: index, length: 1))?.rects.first else {
return
}
let startPoint = self.rightKnob.frame.center
@ -449,7 +449,7 @@ public final class TextSelectionNode: ASDisplayNode {
}
public func setSelection(range: NSRange, displayMenu: Bool) {
guard let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
guard let attributedString = self.textNode.currentText else {
return
}
let range = self.convertSelectionFromOriginalText(attributedString: attributedString, range: range)
@ -561,7 +561,7 @@ public final class TextSelectionNode: ASDisplayNode {
}
public func getSelection() -> NSRange? {
guard let currentRange = self.currentRange, let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
guard let currentRange = self.currentRange, let attributedString = self.textNode.currentText else {
return nil
}
let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1))
@ -573,8 +573,24 @@ public final class TextSelectionNode: ASDisplayNode {
var rects: (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)?
if let range = range {
rects = self.textNode.rangeRects(in: range)
if let range {
if var rectsValue = self.textNode.textRangeRects(in: range) {
var rectList = rectsValue.rects
if rectList.count > 1 {
for i in 0 ..< rectList.count - 1 {
let deltaY = rectList[i + 1].minY - rectList[i].maxY
if deltaY > 0.0 && deltaY <= 4.0 {
rectList[i].size.height += deltaY * 0.5
rectList[i + 1].size.height += deltaY * 0.5
rectList[i + 1].origin.y -= deltaY * 0.5
}
}
}
rectsValue.rects = rectList
rects = rectsValue
} else {
rects = nil
}
}
self.currentRects = rects?.rects
@ -667,7 +683,7 @@ public final class TextSelectionNode: ASDisplayNode {
}
private func displayMenu() {
guard let currentRects = self.currentRects, !currentRects.isEmpty, let currentRange = self.currentRange, let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
guard let currentRects = self.currentRects, !currentRects.isEmpty, let currentRange = self.currentRange, let attributedString = self.textNode.currentText else {
return
}
let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1))