mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Blockquote experiments
This commit is contained in:
parent
cda0334a8b
commit
b4dd3591af
@ -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
|
||||
|
@ -569,6 +569,8 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority;
|
||||
|
||||
@property (nonatomic) bool disableClearContentsOnHide;
|
||||
|
||||
- (void)displayImmediately;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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"];
|
||||
|
@ -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
|
||||
|
@ -46,6 +46,10 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
|
||||
return false;
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
||||
return true;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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?()
|
||||
})
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -1638,6 +1638,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
controllerInteraction: controllerInteraction as! ChatControllerInteraction,
|
||||
selectedMessages: selectedMessages,
|
||||
mode: mode,
|
||||
isChatPreview: false,
|
||||
messageTransitionNode: { return nil }
|
||||
)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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)))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
|
Loading…
x
Reference in New Issue
Block a user