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

View File

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

View File

@ -1715,7 +1715,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
self.inputMenu.back() self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in 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.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in 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 { for (key, value) in attributes {
if let value = value as? ChatTextInputTextQuoteAttribute { if let value = value as? ChatTextInputTextQuoteAttribute {
result.removeAttribute(key, range: range) 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 addAttribute {
if attribute == ChatTextInputAttributes.block { 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 var selectionIndex = nsRange.upperBound
if nsRange.upperBound != result.length && (result.string as NSString).character(at: nsRange.upperBound) != 0x0a { if nsRange.upperBound != result.length && (result.string as NSString).character(at: nsRange.upperBound) != 0x0a {
result.insert(NSAttributedString(string: "\n"), at: nsRange.upperBound) result.insert(NSAttributedString(string: "\n"), at: nsRange.upperBound)
@ -197,6 +197,6 @@ public func chatTextInputAddQuoteAttribute(_ state: ChatTextInputState, selectio
for (attribute, range) in attributesToRemove { for (attribute, range) in attributesToRemove {
result.removeAttribute(attribute, range: range) 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) return ChatTextInputState(inputText: result, selectionRange: selectionRange)
} }

View File

@ -557,14 +557,14 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe
@objc func formatAttributesQuote(_ sender: Any) { @objc func formatAttributesQuote(_ sender: Any) {
self.inputMenu.back() self.inputMenu.back()
if let item = self.item { 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) { @objc func formatAttributesCodeBlock(_ sender: Any) {
self.inputMenu.back() self.inputMenu.back()
if let item = self.item { 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 updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?)
func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?) func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?)
func updateContentsRect(layer: CALayer, contentsRect: CGRect, 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 { 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 ControlledTransitionProperty {
final class AnyValue: Equatable, CustomStringConvertible { final class AnyValue: Equatable, CustomStringConvertible {
let value: Any let value: Any
@ -2232,6 +2311,76 @@ public final class ControlledTransition {
self.updateBounds(layer: layer, bounds: CGRect(origin: CGPoint(), size: frame.size), completion: nil) 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)?) { public func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?) {
if layer.cornerRadius == cornerRadius { if layer.cornerRadius == cornerRadius {
return return
@ -2305,6 +2454,14 @@ public final class ControlledTransition {
self.transition.updatePosition(layer: layer, position: position, completion: completion) 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)?) { 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) 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 self.isNodeLoaded {
if let image = self.image { if let image = self.image {
let capInsets = image.capInsets 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.contentsScale = image.scale
self.contents = image.cgImage self.contents = image.cgImage
} else { } else {

View File

@ -83,14 +83,16 @@ public final class TextNodeBlockQuoteData: NSObject {
public let secondaryColor: UIColor? public let secondaryColor: UIColor?
public let tertiaryColor: UIColor? public let tertiaryColor: UIColor?
public let backgroundColor: 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.kind = kind
self.title = title self.title = title
self.color = color self.color = color
self.secondaryColor = secondaryColor self.secondaryColor = secondaryColor
self.tertiaryColor = tertiaryColor self.tertiaryColor = tertiaryColor
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
self.isCollapsible = isCollapsible
super.init() 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)) 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 struct RenderContentTypes: OptionSet {
public var rawValue: Int public var rawValue: Int
@ -1196,6 +1204,14 @@ open class TextNode: ASDisplayNode {
public internal(set) var cachedLayout: TextNodeLayout? public internal(set) var cachedLayout: TextNodeLayout?
public var renderContentTypes: RenderContentTypes = .all 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() { override public init() {
super.init() super.init()

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import ShimmeringLinkNode
import ChatMessageItemCommon import ChatMessageItemCommon
import TextLoadingEffect import TextLoadingEffect
import ChatControllerInteraction import ChatControllerInteraction
import InteractiveTextComponent
private final class CachedChatMessageText { private final class CachedChatMessageText {
let text: String let text: String
@ -83,8 +84,7 @@ private func findQuoteRange(string: String, quoteText: String, offset: Int?) ->
public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private let containerNode: ASDisplayNode private let containerNode: ASDisplayNode
private let textNode: TextNodeWithEntities private let textNode: InteractiveTextNodeWithEntities
private var spoilerTextNode: TextNodeWithEntities?
private var dustNode: InvisibleInkDustNode? private var dustNode: InvisibleInkDustNode?
private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode 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 codeHighlightState: (id: EngineMessage.Id, specs: [CachedMessageSyntaxHighlight.Spec], disposable: Disposable)?
private var collapsedBlockIds: Set<Int> = Set()
override public var visibility: ListViewItemNodeVisibility { override public var visibility: ListViewItemNodeVisibility {
didSet { didSet {
if oldValue != self.visibility { if oldValue != self.visibility {
switch self.visibility { switch self.visibility {
case .none: case .none:
self.textNode.visibilityRect = nil self.textNode.visibilityRect = nil
self.spoilerTextNode?.visibilityRect = nil
case let .visible(_, subRect): case let .visible(_, subRect):
var subRect = subRect var subRect = subRect
subRect.origin.x = 0.0 subRect.origin.x = 0.0
subRect.size.width = 10000.0 subRect.size.width = 10000.0
self.textNode.visibilityRect = subRect self.textNode.visibilityRect = subRect
self.spoilerTextNode?.visibilityRect = subRect
} }
} }
} }
@ -132,7 +132,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
required public init() { required public init() {
self.containerNode = ASDisplayNode() self.containerNode = ASDisplayNode()
self.textNode = TextNodeWithEntities() self.textNode = InteractiveTextNodeWithEntities()
self.textAccessibilityOverlayNode = TextAccessibilityOverlayNode() self.textAccessibilityOverlayNode = TextAccessibilityOverlayNode()
@ -140,16 +140,28 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.containerNode) self.addSubnode(self.containerNode)
self.textNode.textNode.isUserInteractionEnabled = false self.textNode.textNode.isUserInteractionEnabled = true
self.textNode.textNode.contentMode = .topLeft self.textNode.textNode.contentMode = .topLeft
self.textNode.textNode.contentsScale = UIScreenScale self.textNode.textNode.contentsScale = UIScreenScale
self.textNode.textNode.displaysAsynchronously = true self.textNode.textNode.displaysAsynchronously = true
//self.containerNode.addSubnode(self.textAccessibilityOverlayNode)
self.containerNode.addSubnode(self.textNode.textNode) self.containerNode.addSubnode(self.textNode.textNode)
self.containerNode.addSubnode(self.textAccessibilityOverlayNode)
self.textAccessibilityOverlayNode.openUrl = { [weak self] url in self.textAccessibilityOverlayNode.openUrl = { [weak self] url in
self?.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: false, external: false)) 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) { 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))) { 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 textLayout = InteractiveTextNodeWithEntities.asyncLayout(self.textNode)
let spoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode)
let statusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode) let statusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode)
let currentCachedChatMessageText = self.cachedChatMessageText let currentCachedChatMessageText = self.cachedChatMessageText
let collapsedBlockIds = self.collapsedBlockIds
return { item, layoutConstants, _, _, _, _ in return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) 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) let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil)
var updatedAttributes: [NSAttributedString.Key: Any] = currentDict 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) 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) 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 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 (textLayout, textApply) = textLayout(InteractiveTextNodeLayoutArguments(
attributedString: attributedText,
let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? backgroundColor: nil,
if !textLayout.spoilers.isEmpty { maximumNumberOfLines: maximumNumberOfLines,
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)) truncationType: .end,
} else { constrainedSize: textConstrainedSize,
spoilerTextLayoutAndApply = nil alignment: .natural,
} cutout: nil,
insets: textInsets,
lineColor: messageTheme.accentControlColor,
displayContentsUnderSpoilers: false,
customTruncationToken: customTruncationToken,
collapsedBlocks: collapsedBlockIds
))
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))? var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))?
if let statusType = statusType { if let statusType = statusType {
@ -559,7 +576,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
} }
let trailingWidthToMeasure: CGFloat let trailingWidthToMeasure: CGFloat
if textLayout.hasRTL { if let lastSegment = textLayout.segments.last, lastSegment.hasRTL {
trailingWidthToMeasure = 10000.0 trailingWidthToMeasure = 10000.0
} else { } else {
trailingWidthToMeasure = textLayout.trailingLineWidth trailingWidthToMeasure = textLayout.trailingLineWidth
@ -630,42 +647,20 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: boundingSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: boundingSize)
let cachedLayout = strongSelf.textNode.textNode.cachedLayout let _ = textApply(InteractiveTextNodeWithEntities.Arguments(
context: item.context,
if case .System = animation { cache: item.controllerInteraction.presentationContext.animationCache,
if let cachedLayout = cachedLayout { renderer: item.controllerInteraction.presentationContext.animationRenderer,
if !cachedLayout.areLinesEqual(to: textLayout) { placeholderColor: messageTheme.mediaPlaceholderColor,
if let textContents = strongSelf.textNode.textNode.contents { attemptSynchronous: synchronousLoads,
let fadeNode = ASDisplayNode() textColor: messageTheme.primaryTextColor,
fadeNode.displaysAsynchronously = false spoilerEffectColor: messageTheme.secondaryTextColor,
fadeNode.contents = textContents animation: animation
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))
animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil) animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil)
if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { /*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)) 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))
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
}
strongSelf.spoilerTextNode?.textNode.frame = textFrame strongSelf.spoilerTextNode?.textNode.frame = textFrame
@ -687,18 +682,16 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.dustNode = nil strongSelf.dustNode = nil
dustNode.removeFromSupernode() dustNode.removeFromSupernode()
} }
} }*/
switch strongSelf.visibility { switch strongSelf.visibility {
case .none: case .none:
strongSelf.textNode.visibilityRect = nil strongSelf.textNode.visibilityRect = nil
strongSelf.spoilerTextNode?.visibilityRect = nil
case let .visible(_, subRect): case let .visible(_, subRect):
var subRect = subRect var subRect = subRect
subRect.origin.x = 0.0 subRect.origin.x = 0.0
subRect.size.width = 10000.0 subRect.size.width = 10000.0
strongSelf.textNode.visibilityRect = subRect strongSelf.textNode.visibilityRect = subRect
strongSelf.spoilerTextNode?.visibilityRect = subRect
} }
if let textSelectionNode = strongSelf.textSelectionNode { if let textSelectionNode = strongSelf.textSelectionNode {
@ -710,7 +703,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
strongSelf.textAccessibilityOverlayNode.frame = textFrame strongSelf.textAccessibilityOverlayNode.frame = textFrame
strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout //TODO:localize
//strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
strongSelf.updateIsTranslating(isTranslating) strongSelf.updateIsTranslating(isTranslating)
@ -852,7 +846,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
} }
let textNodeFrame = self.textNode.textNode.frame 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) { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) {
return ChatMessageBubbleContentTapAction(content: .none) return ChatMessageBubbleContentTapAction(content: .none)
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { } 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 { } else if let code = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Code)] as? String {
return ChatMessageBubbleContentTapAction(content: .copy(code)) return ChatMessageBubbleContentTapAction(content: .copy(code))
} else if let _ = attributes[NSAttributedString.Key(rawValue: "Attribute__Blockquote")] { } else if let _ = attributes[NSAttributedString.Key(rawValue: "Attribute__Blockquote")] {
if let text = self.textNode.textNode.attributeSubstring(name: "Attribute__Blockquote", index: index) { if let _ = self.textNode.textNode.collapsibleBlockAtPoint(textLocalPoint) {
return ChatMessageBubbleContentTapAction(content: .copy(text.1))
} else {
return ChatMessageBubbleContentTapAction(content: .none) 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 { } else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file {
return ChatMessageBubbleContentTapAction(content: .customEmoji(file)) return ChatMessageBubbleContentTapAction(content: .customEmoji(file))
@ -1074,7 +1073,17 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
rectsSet = [] rectsSet = []
} }
for i in 0 ..< rectsSet.count { 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 let textHighlightNode: LinkHighlightingNode
if i < self.textHighlightingNodes.count { if i < self.textHighlightingNodes.count {
textHighlightNode = self.textHighlightingNodes[i] textHighlightNode = self.textHighlightingNodes[i]
@ -1302,11 +1311,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
if let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange { if let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, textLayout.segments.contains(where: { !$0.spoilers.isEmpty }), let selectionRange {
for (spoilerRange, _) in textLayout.spoilers { for segment in textLayout.segments {
if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 { for (spoilerRange, _) in segment.spoilers {
dustNode.update(revealed: true) if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 {
return 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) 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) self.updateSpoilersRevealed(animated: animated)
} }
@ -1438,7 +1438,7 @@ extension TextFieldComponent.InputState {
for (key, value) in attributes { for (key, value) in attributes {
if let value = value as? ChatTextInputTextQuoteAttribute { if let value = value as? ChatTextInputTextQuoteAttribute {
result.removeAttribute(key, range: range) 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 addAttribute {
if attribute == ChatTextInputAttributes.block { 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 var selectionIndex = nsRange.upperBound
if nsRange.upperBound != result.length && (result.string as NSString).character(at: nsRange.upperBound) != 0x0a { if nsRange.upperBound != result.length && (result.string as NSString).character(at: nsRange.upperBound) != 0x0a {
result.insert(NSAttributedString(string: "\n"), at: nsRange.upperBound) 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") 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] = [] var rectsSet: [CGRect] = []
if let cachedLayout = textNode.cachedLayout { if let rects = textNode.textRangeRects(in: range)?.rects, !rects.isEmpty {
if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty { rectsSet = rects
rectsSet = rects
}
} }
let maskFrame = CGRect(origin: CGPoint(), size: textNode.bounds.size).insetBy(dx: -4.0, dy: -4.0) 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 historyNodeRotated = true
var isChatPreview = false
switch chatPresentationInterfaceState.mode { switch chatPresentationInterfaceState.mode {
case let .standard(standardMode): case let .standard(standardMode):
if case .embedded(true) = standardMode { if case .embedded(true) = standardMode {
historyNodeRotated = false historyNodeRotated = false
} else if case .previewing = standardMode {
isChatPreview = true
} }
default: default:
break break
@ -614,7 +617,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.controllerInteraction.chatIsRotated = historyNodeRotated self.controllerInteraction.chatIsRotated = historyNodeRotated
var getMessageTransitionNode: (() -> ChatMessageTransitionNodeImpl?)? 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?() return getMessageTransitionNode?()
}) })

View File

@ -718,7 +718,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
return self._isReady.get() 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 var tag = tag
if case .pinnedMessages = subject { if case .pinnedMessages = subject {
tag = .tag(.pinned) tag = .tag(.pinned)
@ -752,13 +752,15 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
self.prefetchManager = InChatPrefetchManager(context: context) self.prefetchManager = InChatPrefetchManager(context: context)
var displayAdPeer: PeerId? var displayAdPeer: PeerId?
switch subject { if !isChatPreview {
case .none, .message: switch subject {
if case let .peer(peerId) = chatLocation { case .none, .message:
displayAdPeer = peerId if case let .peer(peerId) = chatLocation {
displayAdPeer = peerId
}
default:
break
} }
default:
break
} }
var adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> var adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError>
if case .bubbles = mode, let peerId = displayAdPeer { 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.isUserInteractionEnabled = !textInputNode.isUserInteractionEnabled
//self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0]) //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(_:))) let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in recognizer.touchDown = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
@ -4250,7 +4268,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
self.inputMenu.back() self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in 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.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in 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.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 self.historyNode.clipsToBounds = true
super.init() super.init()
@ -566,7 +566,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
} }
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil) 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.clipsToBounds = true
historyNode.preloadPages = true historyNode.preloadPages = true
historyNode.stackFromBottom = true historyNode.stackFromBottom = true

View File

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

View File

@ -249,9 +249,11 @@ public final class ChatTextInputTextQuoteAttribute: NSObject {
} }
public let kind: Kind public let kind: Kind
public let isCollapsed: Bool
public init(kind: Kind) { public init(kind: Kind, isCollapsed: Bool) {
self.kind = kind self.kind = kind
self.isCollapsed = isCollapsed
super.init() super.init()
} }
@ -264,6 +266,9 @@ public final class ChatTextInputTextQuoteAttribute: NSObject {
if self.kind != other.kind { if self.kind != other.kind {
return false return false
} }
if self.isCollapsed != other.isCollapsed {
return false
}
return true return true
} }
@ -646,7 +651,7 @@ private func refreshBlockQuotes(text: NSString, initialAttributedText: NSAttribu
if !quoteRangesEqual(quoteRanges, initialQuoteRanges) { if !quoteRangesEqual(quoteRanges, initialQuoteRanges) {
attributedText.removeAttribute(ChatTextInputAttributes.block, range: fullRange) attributedText.removeAttribute(ChatTextInputAttributes.block, range: fullRange)
for (range, attribute) in quoteRanges { 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 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)) 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 { } else if key == ChatTextInputAttributes.block, let value = value as? ChatTextInputTextQuoteAttribute {
switch value.kind { switch value.kind {
case .quote: 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): case let .code(language):
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Pre(language: 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): case let .CustomEmoji(_, fileId):
string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: range) string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: range)
case let .Pre(language): case let .Pre(language):
string.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: language)), range: range) string.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: language), isCollapsed: false), range: range)
case .BlockQuote: case let .BlockQuote(isCollapsed):
string.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote), range: range) string.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: isCollapsed), range: range)
default: default:
break break
} }
@ -219,12 +219,12 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
if let language, !language.isEmpty { if let language, !language.isEmpty {
title = NSAttributedString(string: language.capitalized, font: boldFont.withSize(round(boldFont.pointSize * 0.8235294117647058)), textColor: codeBlockTitleColor) 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) 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: case .BankCard:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks { if underlineLinks && underlineAllLinks {

View File

@ -218,7 +218,7 @@ public enum TextSelectionAction: Equatable {
public final class TextSelectionNode: ASDisplayNode { public final class TextSelectionNode: ASDisplayNode {
private let theme: TextSelectionTheme private let theme: TextSelectionTheme
private let strings: PresentationStrings private let strings: PresentationStrings
private let textNode: TextNode private let textNode: TextNodeProtocol
private let updateIsActive: (Bool) -> Void private let updateIsActive: (Bool) -> Void
public var canBeginSelection: (CGPoint) -> Bool = { _ in true } public var canBeginSelection: (CGPoint) -> Bool = { _ in true }
public var updateRange: ((NSRange?) -> Void)? public var updateRange: ((NSRange?) -> Void)?
@ -252,7 +252,7 @@ public final class TextSelectionNode: ASDisplayNode {
private weak var contextMenu: ContextMenuController? 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.theme = theme
self.strings = strings self.strings = strings
self.textNode = textNode self.textNode = textNode
@ -302,7 +302,7 @@ public final class TextSelectionNode: ASDisplayNode {
return self?.knobAtPoint(point) return self?.knobAtPoint(point)
} }
recognizer.moveKnob = { [weak self] knob, point in 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 return
} }
@ -335,7 +335,7 @@ public final class TextSelectionNode: ASDisplayNode {
strongSelf.displayMenu() strongSelf.displayMenu()
} }
recognizer.beginSelection = { [weak self] point in 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 return
} }
@ -398,7 +398,7 @@ public final class TextSelectionNode: ASDisplayNode {
} }
public func pretendInitiateSelection() { public func pretendInitiateSelection() {
guard let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else { guard let attributedString = self.textNode.currentText else {
return return
} }
@ -432,7 +432,7 @@ public final class TextSelectionNode: ASDisplayNode {
} }
public func pretendExtendSelection(to index: Int) { 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 return
} }
let startPoint = self.rightKnob.frame.center let startPoint = self.rightKnob.frame.center
@ -449,7 +449,7 @@ public final class TextSelectionNode: ASDisplayNode {
} }
public func setSelection(range: NSRange, displayMenu: Bool) { 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 return
} }
let range = self.convertSelectionFromOriginalText(attributedString: attributedString, range: range) let range = self.convertSelectionFromOriginalText(attributedString: attributedString, range: range)
@ -561,7 +561,7 @@ public final class TextSelectionNode: ASDisplayNode {
} }
public func getSelection() -> NSRange? { 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 return nil
} }
let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1)) 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)? var rects: (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)?
if let range = range { if let range {
rects = self.textNode.rangeRects(in: 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 self.currentRects = rects?.rects
@ -667,7 +683,7 @@ public final class TextSelectionNode: ASDisplayNode {
} }
private func displayMenu() { 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 return
} }
let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1)) let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1))