Quotes experiment

This commit is contained in:
Ali 2023-10-03 23:20:45 +04:00
parent 68a640dc44
commit bab2b39725
26 changed files with 1654 additions and 692 deletions

View File

@ -321,6 +321,7 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
case strikethrough
case underline
case spoiler
case quote
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
@ -348,6 +349,8 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
self = .underline
case 8:
self = .spoiler
case 9:
self = .quote
default:
assertionFailure()
self = .bold
@ -379,6 +382,8 @@ 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:
try container.encode(0 as Int32, forKey: "t")
}
}
}
@ -452,6 +457,9 @@ public struct ChatTextInputStateText: Codable, Equatable {
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .underline, range: range.location ..< (range.location + range.length)))
} else if key == ChatTextInputAttributes.spoiler {
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .spoiler, range: range.location ..< (range.location + range.length)))
} else if key == ChatTextInputAttributes.quote, let value = value as? ChatTextInputTextQuoteAttribute {
let _ = value
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .quote, range: range.location ..< (range.location + range.length)))
}
}
})
@ -496,6 +504,8 @@ 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.quote, value: ChatTextInputTextQuoteAttribute(), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
}
}
return result

View File

@ -16,6 +16,13 @@
@implementation ASCustomTextContainer
- (instancetype)initWithSize:(CGSize)size textStorage:(NSTextStorage *)textStorage {
self = [super initWithSize:size];
if (self != nil) {
}
return self;
}
- (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(nullable CGRect *)remainingRect {
CGRect result = [super lineFragmentRectForProposedRect:proposedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect];
@ -139,8 +146,7 @@
components.layoutManager = layoutManager;
[components.textStorage addLayoutManager:components.layoutManager];
components.textContainer = [[ASCustomTextContainer alloc] initWithSize:textContainerSize];
//components.textContainer.exclusionPaths = @[[UIBezierPath bezierPathWithRect:CGRectMake(textContainerSize.width - 60.0, 0.0, 60.0, 40.0)]];
components.textContainer = [[ASCustomTextContainer alloc] initWithSize:textContainerSize textStorage:textStorage];
components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view.
[components.layoutManager addTextContainer:components.textContainer];

View File

@ -52,6 +52,8 @@ AS_SUBCLASSING_RESTRICTED
@interface ASCustomTextContainer : NSTextContainer
- (instancetype)initWithSize:(CGSize)size textStorage:(NSTextStorage *)textStorage;
@end
#endif

View File

@ -58,7 +58,7 @@
[_textStorage setAttributedString:attributedString];
}
_textContainer = [[ASCustomTextContainer alloc] initWithSize:constrainedSize];
_textContainer = [[ASCustomTextContainer alloc] initWithSize:constrainedSize textStorage:nil];
// We want the text laid out up to the very edges of the container.
_textContainer.lineFragmentPadding = 0;
_textContainer.lineBreakMode = lineBreakMode;

View File

@ -106,3 +106,29 @@ public func chatTextInputAddMentionAttribute(_ state: ChatTextInputState, peer:
return state
}
}
public func chatTextInputAddQuoteAttribute(_ state: ChatTextInputState, selectionRange: Range<Int>) -> ChatTextInputState {
if selectionRange.isEmpty {
return state
}
let nsRange = NSRange(location: selectionRange.lowerBound, length: selectionRange.count)
var quoteRange = nsRange
var attributesToRemove: [(NSAttributedString.Key, NSRange)] = []
state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
for (key, _) in attributes {
if key == ChatTextInputAttributes.quote {
attributesToRemove.append((key, range))
quoteRange = quoteRange.union(range)
} else {
attributesToRemove.append((key, nsRange))
}
}
}
let result = NSMutableAttributedString(attributedString: state.inputText)
for (attribute, range) in attributesToRemove {
result.removeAttribute(attribute, range: range)
}
result.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: nsRange)
return ChatTextInputState(inputText: result, selectionRange: selectionRange)
}

View File

@ -61,8 +61,11 @@ public enum InteractiveTransitionGestureRecognizerEdgeWidth {
}
public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
private let edgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth
private let staticEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth
private let allowedDirections: (CGPoint) -> InteractiveTransitionGestureRecognizerDirections
public var dynamicEdgeWidth: ((CGPoint) -> InteractiveTransitionGestureRecognizerEdgeWidth)?
private var currentEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth
private var validatedGesture = false
private var firstLocation: CGPoint = CGPoint()
@ -70,7 +73,8 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
public init(target: Any?, action: Selector?, allowedDirections: @escaping (CGPoint) -> InteractiveTransitionGestureRecognizerDirections, edgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth = .constant(16.0)) {
self.allowedDirections = allowedDirections
self.edgeWidth = edgeWidth
self.staticEdgeWidth = edgeWidth
self.currentEdgeWidth = edgeWidth
super.init(target: target, action: action)
@ -99,6 +103,10 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
return
}
if let dynamicEdgeWidth = self.dynamicEdgeWidth {
self.currentEdgeWidth = dynamicEdgeWidth(point)
}
super.touchesBegan(touches, with: event)
self.firstLocation = point
@ -151,7 +159,7 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
}
} else {
let edgeWidth: CGFloat
switch self.edgeWidth {
switch self.currentEdgeWidth {
case let .constant(value):
edgeWidth = value
case let .widthMultiplier(factor, minValue, maxValue):

View File

@ -142,6 +142,12 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
}
return .right
})
panRecognizer.dynamicEdgeWidth = { [weak self] _ in
guard let self, let controller = self.controllers.last, let value = controller.interactiveNavivationGestureEdgeWidth else {
return .constant(16.0)
}
return value
}
if #available(iOS 13.4, *) {
panRecognizer.allowedScrollTypesMask = .continuous
}

View File

@ -2,9 +2,14 @@ import Foundation
import UIKit
import AsyncDisplayKit
import CoreText
import AppBundle
private let defaultFont = UIFont.systemFont(ofSize: 15.0)
private let quoteIcon: UIImage = {
return UIImage(bundleImageName: "Chat/Message/ReplyQuoteIcon")!.precomposed()
}()
private final class TextNodeStrikethrough {
let range: NSRange
let frame: CGRect
@ -62,10 +67,48 @@ public struct TextRangeRectEdge: Equatable {
}
}
public final class TextNodeBlockQuoteData: NSObject {
public let id: Int
public let title: NSAttributedString?
public let color: UIColor
public init(id: Int, title: NSAttributedString?, color: UIColor) {
self.id = id
self.title = title
self.color = color
super.init()
}
override public func isEqual(_ object: Any?) -> Bool {
guard let other = object as? TextNodeBlockQuoteData else {
return false
}
if self.id != other.id {
return false
}
if let lhsTitle = self.title, let rhsTitle = other.title {
if !lhsTitle.isEqual(to: rhsTitle) {
return false
}
} else if (self.title == nil) != (other.title == nil) {
return false
}
if !self.color.isEqual(other.color) {
return false
}
return true
}
}
private final class TextNodeLine {
let line: CTLine
let frame: CGRect
let range: NSRange
var frame: CGRect
let ascent: CGFloat
let descent: CGFloat
let range: NSRange?
let isRTL: Bool
let strikethroughs: [TextNodeStrikethrough]
let spoilers: [TextNodeSpoiler]
@ -74,9 +117,11 @@ private final class TextNodeLine {
let attachments: [TextNodeAttachment]
let additionalTrailingLine: (CTLine, Double)?
init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler], spoilerWords: [TextNodeSpoiler], embeddedItems: [TextNodeEmbeddedItem], attachments: [TextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) {
init(line: CTLine, frame: CGRect, ascent: CGFloat, descent: CGFloat, range: NSRange?, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler], spoilerWords: [TextNodeSpoiler], embeddedItems: [TextNodeEmbeddedItem], attachments: [TextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) {
self.line = line
self.frame = frame
self.ascent = ascent
self.descent = descent
self.range = range
self.isRTL = isRTL
self.strikethroughs = strikethroughs
@ -90,9 +135,11 @@ private final class TextNodeLine {
private final class TextNodeBlockQuote {
let frame: CGRect
let tintColor: UIColor
init(frame: CGRect) {
init(frame: CGRect, tintColor: UIColor) {
self.frame = frame
self.tintColor = tintColor
}
}
@ -402,7 +449,14 @@ public final class TextNodeLayout: NSObject {
public var trailingLineWidth: CGFloat {
if let lastLine = self.lines.last {
return lastLine.frame.maxX
var width = lastLine.frame.maxX
for blockQuote in self.blockQuotes {
if lastLine.frame.intersects(blockQuote.frame) {
width = max(width, blockQuote.frame.maxX)
}
}
return width
} else {
return 0.0
}
@ -424,7 +478,7 @@ public final class TextNodeLayout: NSObject {
var closestLine: (Int, CGRect, CGFloat)?
for line in self.lines {
lineIndex += 1
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size)
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size)
switch self.resolvedAlignment {
case .center:
lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0)
@ -492,7 +546,7 @@ public final class TextNodeLayout: NSObject {
var lineIndex = -1
for line in self.lines {
lineIndex += 1
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size)
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size)
switch self.resolvedAlignment {
case .center:
lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0)
@ -559,7 +613,7 @@ public final class TextNodeLayout: NSObject {
}
}
if index >= 0 && index < attributedString.length {
if index < line.range.location + line.range.length {
if let range = line.range, index < range.location + range.length {
return (index, attributedString.attributes(at: index, effectiveRange: nil))
}
}
@ -569,7 +623,7 @@ public final class TextNodeLayout: NSObject {
lineIndex = -1
for line in self.lines {
lineIndex += 1
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size)
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size)
switch self.resolvedAlignment {
case .center:
lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0)
@ -636,7 +690,7 @@ public final class TextNodeLayout: NSObject {
}
}
if index >= 0 && index < attributedString.length {
if index < line.range.location + line.range.length {
if let range = line.range, index < range.location + range.length {
return (index, attributedString.attributes(at: index, effectiveRange: nil))
}
}
@ -667,14 +721,17 @@ public final class TextNodeLayout: NSObject {
var rects: [CGRect] = []
let range = NSRange(stringRange, in: searchText)
for line in self.lines {
let lineRange = NSIntersectionRange(range, line.range)
guard let rangeValue = line.range else {
continue
}
let lineRange = NSIntersectionRange(range, rangeValue)
if lineRange.length != 0 {
var leftOffset: CGFloat = 0.0
if lineRange.location != line.range.location {
if lineRange.location != rangeValue.location {
leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil))
}
var rightOffset: CGFloat = line.frame.width
if lineRange.location + lineRange.length != line.range.length {
if lineRange.location + lineRange.length != rangeValue.length {
var secondaryOffset: CGFloat = 0.0
let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset)
rightOffset = ceil(rawOffset)
@ -682,7 +739,7 @@ public final class TextNodeLayout: NSObject {
rightOffset = ceil(secondaryOffset)
}
}
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size)
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size)
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout)
let width = abs(rightOffset - leftOffset)
@ -716,14 +773,17 @@ public final class TextNodeLayout: NSObject {
if let value = value, range.length != 0 {
var coveringRect = CGRect()
for line in self.lines {
let lineRange = NSIntersectionRange(range, line.range)
guard let rangeValue = line.range else {
continue
}
let lineRange = NSIntersectionRange(range, rangeValue)
if lineRange.length != 0 {
var leftOffset: CGFloat = 0.0
if lineRange.location != line.range.location {
if lineRange.location != rangeValue.location {
leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil))
}
var rightOffset: CGFloat = line.frame.width
if lineRange.location + lineRange.length != line.range.length {
if lineRange.location + lineRange.length != rangeValue.length {
var secondaryOffset: CGFloat = 0.0
let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset)
rightOffset = ceil(rawOffset)
@ -732,7 +792,7 @@ public final class TextNodeLayout: NSObject {
}
}
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size)
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size)
switch self.resolvedAlignment {
case .center:
lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0)
@ -765,14 +825,17 @@ public final class TextNodeLayout: NSObject {
if range.length != 0 {
var rects: [(CGRect, CGRect)] = []
for line in self.lines {
let lineRange = NSIntersectionRange(range, line.range)
guard let rangeValue = line.range else {
continue
}
let lineRange = NSIntersectionRange(range, rangeValue)
if lineRange.length != 0 {
var leftOffset: CGFloat = 0.0
if lineRange.location != line.range.location || line.isRTL {
if lineRange.location != rangeValue.location || line.isRTL {
leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil))
}
var rightOffset: CGFloat = line.frame.width
if lineRange.location + lineRange.length != line.range.length || line.isRTL {
if lineRange.location + lineRange.length != rangeValue.length || line.isRTL {
var secondaryOffset: CGFloat = 0.0
let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset)
rightOffset = ceil(rawOffset)
@ -780,7 +843,7 @@ public final class TextNodeLayout: NSObject {
rightOffset = ceil(secondaryOffset)
}
}
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size)
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size)
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout)
@ -806,14 +869,17 @@ public final class TextNodeLayout: NSObject {
var startEdge: TextRangeRectEdge?
var endEdge: TextRangeRectEdge?
for line in self.lines {
let lineRange = NSIntersectionRange(range, line.range)
guard let rangeValue = line.range else {
continue
}
let lineRange = NSIntersectionRange(range, rangeValue)
if lineRange.length != 0 {
var leftOffset: CGFloat = 0.0
if lineRange.location != line.range.location || line.isRTL {
if lineRange.location != rangeValue.location || line.isRTL {
leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil))
}
var rightOffset: CGFloat = line.frame.width
if lineRange.location + lineRange.length != line.range.upperBound || line.isRTL {
if lineRange.location + lineRange.length != rangeValue.upperBound || line.isRTL {
var secondaryOffset: CGFloat = 0.0
let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset)
rightOffset = ceil(rawOffset)
@ -821,19 +887,19 @@ public final class TextNodeLayout: NSObject {
rightOffset = ceil(secondaryOffset)
}
}
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size)
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size)
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout)
let width = max(0.0, abs(rightOffset - leftOffset))
if line.range.contains(range.lowerBound) {
if rangeValue.contains(range.lowerBound) {
let offsetX = floor(CTLineGetOffsetForStringIndex(line.line, range.lowerBound, nil))
startEdge = TextRangeRectEdge(x: lineFrame.minX + offsetX, y: lineFrame.minY, height: lineFrame.height)
}
if line.range.contains(range.upperBound - 1) {
if rangeValue.contains(range.upperBound - 1) {
let offsetX: CGFloat
if line.range.upperBound == range.upperBound {
if rangeValue.upperBound == range.upperBound {
offsetX = lineFrame.maxX
} else {
var secondaryOffset: CGFloat = 0.0
@ -1022,8 +1088,295 @@ open class TextNode: ASDisplayNode {
}
}
private static func calculateLayoutV2(
attributedString: NSAttributedString,
minimumNumberOfLines: Int,
maximumNumberOfLines: Int,
truncationType: CTLineTruncationType,
backgroundColor: UIColor?,
constrainedSize: CGSize,
alignment: NSTextAlignment,
verticalAlignment: TextVerticalAlignment,
lineSpacingFactor: CGFloat,
cutout: TextNodeCutout?,
insets: UIEdgeInsets,
lineColor: UIColor?,
textShadowColor: UIColor?,
textShadowBlur: CGFloat?,
textStroke: (UIColor, CGFloat)?,
displaySpoilers: Bool,
displayEmbeddedItemsUnderSpoilers: Bool,
customTruncationToken: NSAttributedString?
) -> TextNodeLayout {
let blockQuoteLeftInset: CGFloat = 7.0
let blockQuoteRightInset: CGFloat = 0.0
let blockQuoteIconInset: CGFloat = 12.0
struct StringSegment {
let title: NSAttributedString?
let substring: NSAttributedString
let firstCharacterOffset: Int
let isBlockQuote: Bool
let tintColor: UIColor?
}
var stringSegments: [StringSegment] = []
let rawWholeString = attributedString.string as NSString
let wholeStringLength = rawWholeString.length
var segmentCharacterOffset = 0
while true {
var found = false
attributedString.enumerateAttribute(NSAttributedString.Key("Attribute__Blockquote"), in: NSRange(location: segmentCharacterOffset, length: wholeStringLength - segmentCharacterOffset), using: { value, effectiveRange, _ in
found = true
if segmentCharacterOffset != effectiveRange.location {
stringSegments.append(StringSegment(
title: nil,
substring: attributedString.attributedSubstring(from: NSRange(
location: segmentCharacterOffset,
length: effectiveRange.location - segmentCharacterOffset
)),
firstCharacterOffset: segmentCharacterOffset,
isBlockQuote: false,
tintColor: nil
))
}
if let value = value as? TextNodeBlockQuoteData {
if effectiveRange.length != 0 {
stringSegments.append(StringSegment(
title: value.title,
substring: attributedString.attributedSubstring(from: effectiveRange),
firstCharacterOffset: effectiveRange.location,
isBlockQuote: true,
tintColor: value.color
))
}
} else {
stringSegments.append(StringSegment(
title: nil,
substring: attributedString.attributedSubstring(from: effectiveRange),
firstCharacterOffset: effectiveRange.location,
isBlockQuote: false,
tintColor: nil
))
}
segmentCharacterOffset = effectiveRange.location + effectiveRange.length
})
if !found {
if segmentCharacterOffset != wholeStringLength {
stringSegments.append(StringSegment(
title: nil,
substring: attributedString.attributedSubstring(from: NSRange(
location: segmentCharacterOffset,
length: wholeStringLength - segmentCharacterOffset
)),
firstCharacterOffset: segmentCharacterOffset,
isBlockQuote: false,
tintColor: nil
))
}
break
}
}
struct CalculatedSegment {
var titleLine: TextNodeLine?
var lines: [TextNodeLine] = []
var tintColor: UIColor?
var isBlockQuote: Bool = false
var additionalWidth: CGFloat = 0.0
}
var calculatedSegments: [CalculatedSegment] = []
for segment in stringSegments {
var calculatedSegment = CalculatedSegment()
calculatedSegment.isBlockQuote = segment.isBlockQuote
calculatedSegment.tintColor = segment.tintColor
let rawSubstring = segment.substring.string as NSString
let substringLength = rawSubstring.length
let typesetter = CTTypesetterCreateWithAttributedString(segment.substring as CFAttributedString)
var currentLineStartIndex = 0
var constrainedSegmentWidth = constrainedSize.width
var additionalOffsetX: CGFloat = 0.0
if segment.isBlockQuote {
constrainedSegmentWidth -= blockQuoteLeftInset + blockQuoteRightInset
additionalOffsetX += blockQuoteLeftInset
calculatedSegment.additionalWidth += blockQuoteLeftInset + blockQuoteRightInset
}
var additionalSegmentRightInset = blockQuoteIconInset
if let title = segment.title {
let rawTitleLine = CTLineCreateWithAttributedString(title)
if let titleLine = CTLineCreateTruncatedLine(rawTitleLine, constrainedSegmentWidth + additionalSegmentRightInset, .end, nil) {
var lineAscent: CGFloat = 0.0
var lineDescent: CGFloat = 0.0
let lineWidth = CTLineGetTypographicBounds(titleLine, &lineAscent, &lineDescent, nil)
calculatedSegment.titleLine = TextNodeLine(
line: titleLine,
frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)),
ascent: lineAscent,
descent: lineDescent,
range: nil,
isRTL: false,
strikethroughs: [],
spoilers: [],
spoilerWords: [],
embeddedItems: [],
attachments: [],
additionalTrailingLine: nil
)
additionalSegmentRightInset = 0.0
}
}
while true {
let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, currentLineStartIndex, constrainedSegmentWidth + additionalSegmentRightInset)
if lineCharacterCount != 0 {
let line = CTTypesetterCreateLine(typesetter, CFRange(location: currentLineStartIndex, length: lineCharacterCount))
var lineAscent: CGFloat = 0.0
var lineDescent: CGFloat = 0.0
let lineWidth = CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, nil)
calculatedSegment.lines.append(TextNodeLine(
line: line,
frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)),
ascent: lineAscent,
descent: lineDescent,
range: NSRange(location: segment.firstCharacterOffset + currentLineStartIndex, length: lineCharacterCount),
isRTL: false,
strikethroughs: [],
spoilers: [],
spoilerWords: [],
embeddedItems: [],
attachments: [],
additionalTrailingLine: nil
))
}
additionalSegmentRightInset = 0.0
currentLineStartIndex += lineCharacterCount
if currentLineStartIndex >= substringLength {
break
}
}
calculatedSegments.append(calculatedSegment)
}
var size = CGSize()
let isTruncated = false
for segment in calculatedSegments {
if let titleLine = segment.titleLine {
size.width = max(size.width, titleLine.frame.origin.x + titleLine.frame.width + segment.additionalWidth)
}
for line in segment.lines {
size.width = max(size.width, line.frame.origin.x + line.frame.width + segment.additionalWidth)
}
}
var lines: [TextNodeLine] = []
var blockQuotes: [TextNodeBlockQuote] = []
for i in 0 ..< calculatedSegments.count {
let segment = calculatedSegments[i]
if i != 0 {
if segment.isBlockQuote {
size.height += 6.0
}
} else {
if segment.isBlockQuote {
size.height += 7.0
}
}
let blockMinY = size.height - insets.bottom
var blockWidth: CGFloat = 0.0
if let titleLine = segment.titleLine {
titleLine.frame = CGRect(origin: CGPoint(x: titleLine.frame.origin.x + insets.left, y: -insets.bottom + size.height + titleLine.frame.size.height), size: titleLine.frame.size)
titleLine.frame.size.width += max(0.0, segment.additionalWidth - 2.0)
size.height += titleLine.frame.height
blockWidth = max(blockWidth, titleLine.frame.origin.x + titleLine.frame.width)
lines.append(titleLine)
}
for line in segment.lines {
line.frame = CGRect(origin: CGPoint(x: line.frame.origin.x + insets.left, y: -insets.bottom + size.height + line.frame.size.height), size: line.frame.size)
line.frame.size.width += max(0.0, segment.additionalWidth - 2.0)
//line.frame.size.width = max(blockWidth, line.frame.size.width)
size.height += line.frame.height
blockWidth = max(blockWidth, line.frame.origin.x + line.frame.width)
lines.append(line)
}
let blockMaxY = size.height - insets.bottom
if i != calculatedSegments.count - 1 {
if segment.isBlockQuote {
size.height += 8.0
}
} else {
if segment.isBlockQuote {
size.height += 6.0
}
}
if segment.isBlockQuote, let tintColor = segment.tintColor {
blockQuotes.append(TextNodeBlockQuote(frame: CGRect(origin: CGPoint(x: 0.0, y: blockMinY - 2.0), size: CGSize(width: blockWidth, height: blockMaxY - (blockMinY - 2.0) + 4.0)), tintColor: tintColor))
}
}
let rawTextSize = size
size.width += insets.left + insets.right
size.height += insets.top + insets.bottom
return TextNodeLayout(
attributedString: attributedString,
maximumNumberOfLines: maximumNumberOfLines,
truncationType: truncationType,
constrainedSize: constrainedSize,
explicitAlignment: alignment,
resolvedAlignment: alignment,
verticalAlignment: verticalAlignment,
lineSpacing: lineSpacingFactor,
cutout: cutout,
insets: insets,
size: size,
rawTextSize: rawTextSize,
truncated: isTruncated,
firstLineOffset: lines.first?.descent ?? 0.0,
lines: lines,
blockQuotes: blockQuotes,
backgroundColor: backgroundColor,
lineColor: lineColor,
textShadowColor: textShadowColor,
textShadowBlur: textShadowBlur,
textStroke: textStroke,
displaySpoilers: displaySpoilers
)
}
static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?) -> TextNodeLayout {
if let attributedString = attributedString {
guard let attributedString else {
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers)
}
if "".isEmpty, maximumNumberOfLines == 0 {
return calculateLayoutV2(attributedString: attributedString, minimumNumberOfLines: minimumNumberOfLines, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, verticalAlignment: verticalAlignment, lineSpacingFactor: lineSpacingFactor, cutout: cutout, insets: insets, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers, displayEmbeddedItemsUnderSpoilers: displayEmbeddedItemsUnderSpoilers, customTruncationToken: customTruncationToken)
}
let stringLength = attributedString.length
let font: CTFont
@ -1055,7 +1408,7 @@ open class TextNode: ASDisplayNode {
let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor)
var lines: [TextNodeLine] = []
var blockQuotes: [TextNodeBlockQuote] = []
let blockQuotes: [TextNodeBlockQuote] = []
var maybeTypesetter: CTTypesetter?
maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
@ -1390,7 +1743,9 @@ open class TextNode: ASDisplayNode {
}
}
let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))))
var lineAscent: CGFloat = 0.0
var lineDescent: CGFloat = 0.0
let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))))
let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight)
layoutSize.height += fontLineHeight + fontLineSpacing
@ -1401,7 +1756,7 @@ open class TextNode: ASDisplayNode {
layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth)
if headIndent > 0.0 {
blockQuotes.append(TextNodeBlockQuote(frame: lineFrame))
//blockQuotes.append(TextNodeBlockQuote(frame: lineFrame))
}
var isRTL = false
@ -1413,7 +1768,20 @@ open class TextNode: ASDisplayNode {
}
}
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(effectiveLineRange.location, effectiveLineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: embeddedItems, attachments: attachments, additionalTrailingLine: additionalTrailingLine))
lines.append(TextNodeLine(
line: coreTextLine,
frame: lineFrame,
ascent: lineAscent,
descent: lineDescent,
range: NSMakeRange(effectiveLineRange.location, effectiveLineRange.length),
isRTL: isRTL,
strikethroughs: strikethroughs,
spoilers: spoilers,
spoilerWords: spoilerWords,
embeddedItems: embeddedItems,
attachments: attachments,
additionalTrailingLine: additionalTrailingLine
))
break
} else {
if lineCharacterCount > 0 {
@ -1493,13 +1861,15 @@ open class TextNode: ASDisplayNode {
}
}
let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))
var lineAscent: CGFloat = 0.0
var lineDescent: CGFloat = 0.0
let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))
let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight)
layoutSize.height += fontLineHeight
layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth)
layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth + headIndent)
if headIndent > 0.0 {
blockQuotes.append(TextNodeBlockQuote(frame: lineFrame))
//blockQuotes.append(TextNodeBlockQuote(frame: lineFrame))
}
var isRTL = false
@ -1511,7 +1881,20 @@ open class TextNode: ASDisplayNode {
}
}
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: embeddedItems, attachments: attachments, additionalTrailingLine: nil))
lines.append(TextNodeLine(
line: coreTextLine,
frame: lineFrame,
ascent: lineAscent,
descent: lineDescent,
range: NSMakeRange(lineRange.location, lineRange.length),
isRTL: isRTL,
strikethroughs: strikethroughs,
spoilers: spoilers,
spoilerWords: spoilerWords,
embeddedItems: embeddedItems,
attachments: attachments,
additionalTrailingLine: nil
))
} else {
if !lines.isEmpty {
layoutSize.height += fontLineSpacing
@ -1545,9 +1928,6 @@ open class TextNode: ASDisplayNode {
}
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers)
} else {
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers)
}
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
@ -1584,6 +1964,46 @@ open class TextNode: ASDisplayNode {
context.fill(bounds)
}
let alignment = layout.resolvedAlignment
var offset = CGPoint(x: layout.insets.left, y: layout.insets.top)
switch layout.verticalAlignment {
case .top:
break
case .middle:
offset.y = floor((bounds.height - layout.size.height) / 2.0) + layout.insets.top
case .bottom:
offset.y = floor(bounds.height - layout.size.height) + layout.insets.top
}
if !layout.lines.isEmpty {
offset.y += layout.lines[0].descent
}
for blockQuote in layout.blockQuotes {
let radius: CGFloat = 3.0
let blockFrame = blockQuote.frame.offsetBy(dx: offset.x + 2.0, dy: offset.y)
context.setFillColor(blockQuote.tintColor.withMultipliedAlpha(0.1).cgColor)
context.addPath(UIBezierPath(roundedRect: blockFrame, cornerRadius: radius).cgPath)
context.fillPath()
context.setFillColor(blockQuote.tintColor.cgColor)
let quoteRect = CGRect(origin: CGPoint(x: blockFrame.maxX - 4.0 - quoteIcon.size.width, y: blockFrame.minY + 4.0), size: quoteIcon.size)
context.clip(to: quoteRect, mask: quoteIcon.cgImage!)
context.fill(quoteRect)
context.resetClip()
let lineFrame = CGRect(origin: CGPoint(x: blockFrame.minX, y: blockFrame.minY), size: CGSize(width: radius, height: blockFrame.height))
context.move(to: CGPoint(x: lineFrame.minX, y: lineFrame.minY + radius))
context.addArc(tangent1End: CGPoint(x: lineFrame.minX, y: lineFrame.minY), tangent2End: CGPoint(x: lineFrame.minX + radius, y: lineFrame.minY), radius: radius)
context.addLine(to: CGPoint(x: lineFrame.minX + radius, y: lineFrame.maxY))
context.addArc(tangent1End: CGPoint(x: lineFrame.minX, y: lineFrame.maxY), tangent2End: CGPoint(x: lineFrame.minX, y: lineFrame.maxY - radius), radius: radius)
context.closePath()
context.fillPath()
}
if let textShadowColor = layout.textShadowColor {
context.setTextDrawingMode(.fill)
context.setShadow(offset: layout.textShadowBlur != nil ? .zero : CGSize(width: 0.0, height: 1.0), blur: layout.textShadowBlur ?? 0.0, color: textShadowColor.cgColor)
@ -1605,17 +2025,6 @@ open class TextNode: ASDisplayNode {
let textPosition = context.textPosition
context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0)
let alignment = layout.resolvedAlignment
var offset = CGPoint(x: layout.insets.left, y: layout.insets.top)
switch layout.verticalAlignment {
case .top:
break
case .middle:
offset.y = floor((bounds.height - layout.size.height) / 2.0) + layout.insets.top
case .bottom:
offset.y = floor(bounds.height - layout.size.height) + layout.insets.top
}
for i in 0 ..< layout.lines.count {
let line = layout.lines[i]
@ -1632,6 +2041,12 @@ open class TextNode: ASDisplayNode {
lineFrame.origin.x += offset.x
}
}
//context.setStrokeColor(UIColor.red.cgColor)
//context.stroke(lineFrame.offsetBy(dx: 0.0, dy: -lineFrame.height))
lineFrame.origin.y += -line.descent
context.textPosition = CGPoint(x: lineFrame.minX, y: lineFrame.minY)
if layout.displaySpoilers && !line.spoilers.isEmpty {
@ -1688,8 +2103,11 @@ open class TextNode: ASDisplayNode {
if !line.strikethroughs.isEmpty {
for strikethrough in line.strikethroughs {
guard let lineRange = line.range else {
continue
}
var textColor: UIColor?
layout.attributedString?.enumerateAttributes(in: NSMakeRange(line.range.location, line.range.length), options: []) { attributes, range, _ in
layout.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in
if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor {
textColor = color
}
@ -1760,34 +2178,6 @@ open class TextNode: ASDisplayNode {
}
}
var blockQuoteFrames: [CGRect] = []
var currentBlockQuoteFrame: CGRect?
for blockQuote in layout.blockQuotes {
if let frame = currentBlockQuoteFrame {
if blockQuote.frame.minY - frame.maxY < 20.0 {
currentBlockQuoteFrame = frame.union(blockQuote.frame)
} else {
blockQuoteFrames.append(frame)
currentBlockQuoteFrame = frame
}
} else {
currentBlockQuoteFrame = blockQuote.frame
}
}
if let frame = currentBlockQuoteFrame {
blockQuoteFrames.append(frame)
}
for frame in blockQuoteFrames {
if let lineColor = layout.lineColor {
context.setFillColor(lineColor.cgColor)
}
let rect = UIBezierPath(roundedRect: CGRect(x: frame.minX - 9.0, y: frame.minY - 14.0, width: 2.0, height: frame.height), cornerRadius: 1.0)
context.addPath(rect.cgPath)
context.fillPath()
}
context.textMatrix = textMatrix
context.textPosition = CGPoint(x: textPosition.x, y: textPosition.y)
}
@ -1944,7 +2334,7 @@ open class TextView: UIView {
let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor)
var lines: [TextNodeLine] = []
var blockQuotes: [TextNodeBlockQuote] = []
let blockQuotes: [TextNodeBlockQuote] = []
var maybeTypesetter: CTTypesetter?
maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
@ -2175,13 +2565,15 @@ open class TextView: UIView {
}
}
let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))))
var lineAscent: CGFloat = 0.0
var lineDescent: CGFloat = 0.0
let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))))
let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight)
layoutSize.height += fontLineHeight + fontLineSpacing
layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth)
if headIndent > 0.0 {
blockQuotes.append(TextNodeBlockQuote(frame: lineFrame))
//blockQuotes.append(TextNodeBlockQuote(frame: lineFrame))
}
var isRTL = false
@ -2193,7 +2585,20 @@ open class TextView: UIView {
}
}
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: [], attachments: attachments, additionalTrailingLine: nil))
lines.append(TextNodeLine(
line: coreTextLine,
frame: lineFrame,
ascent: lineAscent,
descent: lineDescent,
range: NSMakeRange(lineRange.location, lineRange.length),
isRTL: isRTL,
strikethroughs: strikethroughs,
spoilers: spoilers,
spoilerWords: spoilerWords,
embeddedItems: [],
attachments: attachments,
additionalTrailingLine: nil
))
break
} else {
if lineCharacterCount > 0 {
@ -2263,13 +2668,15 @@ open class TextView: UIView {
}
}
let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))
var lineAscent: CGFloat = 0.0
var lineDescent: CGFloat = 0.0
let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))
let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight)
layoutSize.height += fontLineHeight
layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth)
if headIndent > 0.0 {
blockQuotes.append(TextNodeBlockQuote(frame: lineFrame))
//blockQuotes.append(TextNodeBlockQuote(frame: lineFrame))
}
var isRTL = false
@ -2281,7 +2688,20 @@ open class TextView: UIView {
}
}
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: [], attachments: attachments, additionalTrailingLine: nil))
lines.append(TextNodeLine(
line: coreTextLine,
frame: lineFrame,
ascent: lineAscent,
descent: lineDescent,
range: NSMakeRange(lineRange.location, lineRange.length),
isRTL: isRTL,
strikethroughs: strikethroughs,
spoilers: spoilers,
spoilerWords: spoilerWords,
embeddedItems: [],
attachments: attachments,
additionalTrailingLine: nil
))
} else {
if !lines.isEmpty {
layoutSize.height += fontLineSpacing

View File

@ -228,6 +228,10 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
private var navigationBarOrigin: CGFloat = 0.0
open var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? {
return nil
}
open func navigationLayout(layout: ContainerViewLayout) -> NavigationLayout {
let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0
var defaultNavigationBarHeight: CGFloat

View File

@ -64,6 +64,9 @@ private func chatInputStateString(attributedString: NSAttributedString) -> NSAtt
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
string.addAttribute(ChatTextInputAttributes.customEmoji, value: value, range: range)
}
if let value = attributes[ChatTextInputAttributes.quote] as? ChatTextInputTextQuoteAttribute {
string.addAttribute(ChatTextInputAttributes.quote, value: value, range: range)
}
})
return string
}

View File

@ -156,6 +156,9 @@ public extension TelegramEngine {
}
}
/*public func subscribe<each T: TelegramEngineDataItem>(_ ts: repeat each T) -> Signal<repeat each T, NoError> {
}*/
public func subscribe<T0: TelegramEngineDataItem>(_ t0: T0) -> Signal<T0.Result, NoError> {
return self._subscribe(items: [t0 as! AnyPostboxViewDataItem])
|> map { results -> T0.Result in

View File

@ -34,6 +34,8 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/MessageQuoteComponent",
"//submodules/TelegramUI/Components/RichTextView",
],
visibility = [
"//visibility:public",

View File

@ -24,6 +24,7 @@ import ChatMessageDateAndStatusNode
import ChatMessageBubbleContentNode
import ShimmeringLinkNode
import ChatMessageItemCommon
import RichTextView
private final class CachedChatMessageText {
let text: String
@ -343,7 +344,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let textFont = item.presentationData.messageFont
if let entities = entities {
attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, message: item.message)
attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseQuoteTintColor: messageTheme.accentControlColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, message: item.message)
} else if !rawText.isEmpty {
attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.primaryTextColor)
} else {
@ -609,6 +610,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return .bankCard(bankCard)
} else if let pre = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre)] as? String {
return .copy(pre)
} else if let code = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Code)] as? String {
return .copy(code)
} else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file {
return .customEmoji(file)
} else {

View File

@ -0,0 +1,21 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MessageQuoteComponent",
module_name = "MessageQuoteComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/TelegramCore",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,67 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import TelegramCore
private let lineImage: UIImage = {
let radius: CGFloat = 4.0
return generateImage(CGSize(width: radius, height: radius * 2.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
})!.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(radius)).withRenderingMode(.alwaysTemplate)
}()
public final class MessageQuoteView: UIView {
public struct Params {
let presentationData: ChatPresentationData
let authorName: String?
let text: String
let entities: [MessageTextEntity]
public init(
presentationData: ChatPresentationData,
authorName: String?,
text: String,
entities: [MessageTextEntity]
) {
self.presentationData = presentationData
self.authorName = authorName
self.text = text
self.entities = entities
}
}
private let lineView: UIImageView
override private init(frame: CGRect) {
self.lineView = UIImageView()
self.lineView.image = lineImage
super.init(frame: frame)
self.addSubview(self.lineView)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public static func asyncLayout(_ view: MessageQuoteView?) -> (Params) -> (CGSize, (CGSize) -> MessageQuoteView) {
return { params in
var minSize = CGSize()
minSize.height = 100.0
return (minSize, { size in
let view = view ?? MessageQuoteView(frame: CGRect())
view.lineView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: lineImage.size.width, height: size.height))
return view
})
}
}
}

View File

@ -0,0 +1,17 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "RichTextView",
module_name = "RichTextView",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,82 @@
import Foundation
import UIKit
public final class RichTextView: UIView {
public final class Params: Equatable {
let string: NSAttributedString
let constrainedSize: CGSize
public init(
string: NSAttributedString,
constrainedSize: CGSize
) {
self.string = string
self.constrainedSize = constrainedSize
}
public static func ==(lhs: Params, rhs: Params) -> Bool {
if !lhs.string.isEqual(to: rhs.string) {
return false
}
if lhs.constrainedSize != rhs.constrainedSize {
return false
}
return true
}
}
public final class LayoutData: Equatable {
init() {
}
public static func ==(lhs: LayoutData, rhs: LayoutData) -> Bool {
return true
}
}
public final class AsyncResult {
public let view: () -> RichTextView
public let layoutData: LayoutData
init(view: @escaping () -> RichTextView, layoutData: LayoutData) {
self.view = view
self.layoutData = layoutData
}
}
private static func performLayout(params: Params) -> LayoutData {
return LayoutData()
}
public static func updateAsync(_ view: RichTextView?) -> (Params) -> AsyncResult {
return { params in
let layoutData = performLayout(params: params)
return AsyncResult(
view: {
let view = view ?? RichTextView(frame: CGRect())
view.layoutData = layoutData
return view
},
layoutData: layoutData
)
}
}
private var layoutData: LayoutData?
override public init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func draw(_ rect: CGRect) {
guard let layoutData = self.layoutData else {
return
}
let _ = layoutData
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "quotemini.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,158 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 9.000000 7.000244 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.141113 cm
0.000000 0.000000 0.000000 scn
0.000000 5.141357 m
0.000000 6.245927 0.895431 7.141357 2.000000 7.141357 c
3.104569 7.141357 4.000000 6.245927 4.000000 5.141357 c
4.000000 4.557765 l
4.000000 3.471050 3.746984 2.399258 3.260991 1.427270 c
2.894427 0.694144 l
2.647438 0.200165 2.046765 -0.000059 1.552786 0.246930 c
1.058808 0.493919 0.858584 1.094593 1.105573 1.588571 c
1.472136 2.321697 l
1.605720 2.588866 1.714662 2.866591 1.798144 3.151417 c
0.788369 3.252621 0.000000 4.104923 0.000000 5.141357 c
h
5.000000 5.141357 m
5.000000 6.245927 5.895431 7.141357 7.000000 7.141357 c
8.104569 7.141357 9.000000 6.245927 9.000000 5.141357 c
9.000000 4.557765 l
9.000000 3.471050 8.746984 2.399258 8.260990 1.427270 c
7.894427 0.694144 l
7.647438 0.200165 7.046765 -0.000059 6.552786 0.246930 c
6.058808 0.493919 5.858583 1.094593 6.105573 1.588571 c
6.472136 2.321697 l
6.605721 2.588866 6.714662 2.866591 6.798144 3.151417 c
5.788369 3.252621 5.000000 4.104923 5.000000 5.141357 c
h
f*
n
Q
endstream
endobj
2 0 obj
1077
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 9.000000 7.000244 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 7.000244 m
9.000000 7.000244 l
9.000000 0.000025 l
0.000000 0.000025 l
0.000000 7.000244 l
h
f
n
Q
endstream
endobj
4 0 obj
227
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
endstream
endobj
7 0 obj
46
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 9.000000 7.000244 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000001333 00000 n
0000001356 00000 n
0000001829 00000 n
0000001851 00000 n
0000002149 00000 n
0000002251 00000 n
0000002272 00000 n
0000002443 00000 n
0000002517 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
2577
%%EOF

View File

@ -529,6 +529,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
override public var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? {
return .widthMultiplier(factor: 0.35, min: 16.0, max: 200.0)
}
private var scheduledScrollToMessageId: (MessageId, Double?)?
public var purposefulAction: (() -> Void)?

View File

@ -1,122 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import UIKit
import SwiftSignalKit
import Photos
import TelegramPresentationData
import UIKitRuntimeUtils
final class ChatDateSelectionSheet: ActionSheetController {
private let strings: PresentationStrings
private let _ready = Promise<Bool>()
override var ready: Promise<Bool> {
return self._ready
}
init(presentationData: PresentationData, completion: @escaping (Int32) -> Void) {
self.strings = presentationData.strings
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
self._ready.set(.single(true))
var updatedValue: Int32?
self.setItemGroups([
ActionSheetItemGroup(items: [
ChatDateSelectorItem(strings: self.strings, valueChanged: { value in
updatedValue = value
}),
ActionSheetButtonItem(title: self.strings.Common_Search, action: { [weak self] in
self?.dismissAnimated()
if let updatedValue = updatedValue {
completion(updatedValue)
}
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
}),
])
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private final class ChatDateSelectorItem: ActionSheetItem {
let strings: PresentationStrings
let valueChanged: (Int32) -> Void
init(strings: PresentationStrings, valueChanged: @escaping (Int32) -> Void) {
self.strings = strings
self.valueChanged = valueChanged
}
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return ChatDateSelectorItemNode(theme: theme, strings: self.strings, valueChanged: self.valueChanged)
}
func updateNode(_ node: ActionSheetItemNode) {
}
}
private final class ChatDateSelectorItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let strings: PresentationStrings
private let pickerView: UIDatePicker
private let valueChanged: (Int32) -> Void
private var currentValue: Int32 {
return Int32(self.pickerView.date.timeIntervalSince1970)
}
init(theme: ActionSheetControllerTheme, strings: PresentationStrings, valueChanged: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.valueChanged = valueChanged
UILabel.setDateLabel(theme.primaryTextColor)
self.pickerView = UIDatePicker()
self.pickerView.datePickerMode = .countDownTimer
self.pickerView.datePickerMode = .date
self.pickerView.locale = Locale(identifier: strings.baseLanguageCode)
self.pickerView.minimumDate = Date(timeIntervalSince1970: 1376438400.0)
self.pickerView.maximumDate = Date(timeIntervalSinceNow: 2.0)
if #available(iOS 13.4, *) {
self.pickerView.preferredDatePickerStyle = .wheels
}
self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor")
self.pickerView.setValue(theme.primaryTextColor, forKey: "highlightColor")
super.init(theme: theme)
self.view.addSubview(self.pickerView)
self.pickerView.addTarget(self, action: #selector(self.pickerChanged), for: .valueChanged)
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 157.0)
self.pickerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 180.0))
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc func pickerChanged() {
self.valueChanged(self.currentValue)
}
}

View File

@ -3568,7 +3568,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
return ASEditableTextNodeTargetForAction(target: nil)
}
}
} else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) || action == #selector(self.formatAttributesSpoiler(_:)) {
} else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) || action == #selector(self.formatAttributesSpoiler(_:)) || action == #selector(self.formatAttributesQuote(_:)) {
if case .format = self.inputMenu.state {
if action == #selector(self.formatAttributesSpoiler(_:)), let selectedRange = self.textInputNode?.selectedRange {
var intersectsMonospace = false
@ -3582,6 +3582,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
} else {
return ASEditableTextNodeTargetForAction(target: nil)
}
} else if action == #selector(self.formatAttributesQuote(_:)), let selectedRange = self.textInputNode?.selectedRange {
let _ = selectedRange
return ASEditableTextNodeTargetForAction(target: self)
} else if action == #selector(self.formatAttributesMonospace(_:)), let selectedRange = self.textInputNode?.selectedRange {
var intersectsSpoiler = false
self.inputTextState.inputText.enumerateAttributes(in: selectedRange, options: [], using: { attributes, _, _ in
@ -3614,7 +3617,29 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 {
} else {
var children: [UIAction] = [
var children: [UIAction] = []
//TODO:localize
children.append(UIAction(title: "Quote", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesQuote(strongSelf)
}
})
var hasSpoilers = true
if self.presentationInterfaceState?.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
hasSpoilers = false
}
if hasSpoilers {
children.append(UIAction(title: self.strings?.TextFormat_Spoiler ?? "Spoiler", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesSpoiler(strongSelf)
}
})
}
children.append(contentsOf: [
UIAction(title: self.strings?.TextFormat_Bold ?? "Bold", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesBold(strongSelf)
@ -3645,20 +3670,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
strongSelf.formatAttributesUnderline(strongSelf)
}
}
]
var hasSpoilers = true
if self.presentationInterfaceState?.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
hasSpoilers = false
}
if hasSpoilers {
children.append(UIAction(title: self.strings?.TextFormat_Spoiler ?? "Spoiler", image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesSpoiler(strongSelf)
}
})
}
] as [UIAction])
let formatMenu = UIMenu(title: self.strings?.TextFormat_Format ?? "Format", image: nil, children: children)
actions.insert(formatMenu, at: 3)
@ -3737,6 +3749,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
}
}
@objc func formatAttributesQuote(_ sender: Any) {
self.inputMenu.back()
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.quote), inputMode)
}
}
@objc func formatAttributesSpoiler(_ sender: Any) {
self.inputMenu.back()

View File

@ -19,8 +19,10 @@ public struct ChatTextInputAttributes {
public static let textUrl = NSAttributedString.Key(rawValue: "Attribute__TextUrl")
public static let spoiler = NSAttributedString.Key(rawValue: "Attribute__Spoiler")
public static let customEmoji = NSAttributedString.Key(rawValue: "Attribute__CustomEmoji")
public static let code = NSAttributedString.Key(rawValue: "Attribute__Code")
public static let quote = NSAttributedString.Key(rawValue: "Attribute__Blockquote")
public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler, ChatTextInputAttributes.customEmoji]
public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler, ChatTextInputAttributes.customEmoji, ChatTextInputAttributes.code, ChatTextInputAttributes.quote]
}
public let originalTextAttributeKey = NSAttributedString.Key(rawValue: "Attribute__OriginalText")
@ -115,6 +117,13 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo
} else if key == ChatTextInputAttributes.customEmoji {
result.addAttribute(key, value: value, range: range)
result.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
} else if key == ChatTextInputAttributes.quote {
result.addAttribute(key, value: value, range: range)
result.addAttribute(NSAttributedString.Key.backgroundColor, value: accentTextColor.withAlphaComponent(0.15), range: range)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.headIndent = 8.0
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])]
result.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range)
}
}
@ -193,6 +202,22 @@ public final class ChatTextInputTextUrlAttribute: NSObject {
}
}
public final class ChatTextInputTextQuoteAttribute: NSObject {
override public init() {
super.init()
}
override public func isEqual(_ object: Any?) -> Bool {
guard let other = object as? ChatTextInputTextQuoteAttribute else {
return false
}
let _ = other
return true
}
}
public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable {
private enum CodingKeys: String, CodingKey {
case interactivelySelectedFromPackId
@ -506,6 +531,135 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed
}
}
private func quoteRangesEqual(_ lhs: [(NSRange, ChatTextInputTextQuoteAttribute)], _ rhs: [(NSRange, ChatTextInputTextQuoteAttribute)]) -> Bool {
if lhs.count != rhs.count {
return false
}
for i in 0 ..< lhs.count {
if lhs[i].0 != rhs[i].0 || !lhs[i].1.isEqual(rhs[i].1) {
return false
}
}
return true
}
private func refreshBlockQuotes(text: NSString, initialAttributedText: NSAttributedString, attributedText: NSMutableAttributedString, fullRange: NSRange) {
var quoteRanges: [(NSRange, ChatTextInputTextQuoteAttribute)] = []
initialAttributedText.enumerateAttribute(ChatTextInputAttributes.quote, in: fullRange, options: [], using: { value, range, _ in
if let value = value as? ChatTextInputTextQuoteAttribute {
quoteRanges.append((range, value))
}
})
quoteRanges.sort(by: { $0.0.location < $1.0.location })
let initialQuoteRanges = quoteRanges
for i in 0 ..< quoteRanges.count {
let range = quoteRanges[i].0
var validLower = range.lowerBound
inner1: for i in range.lowerBound ..< range.upperBound {
if let c = UnicodeScalar(text.character(at: i)) {
if textUrlCharacters.contains(c) {
validLower = i
break inner1
}
} else {
break inner1
}
}
var validUpper = range.upperBound
inner2: for i in (validLower ..< range.upperBound).reversed() {
if let c = UnicodeScalar(text.character(at: i)) {
if textUrlCharacters.contains(c) {
validUpper = i + 1
break inner2
}
} else {
break inner2
}
}
let minLower = (i == 0) ? fullRange.lowerBound : quoteRanges[i - 1].0.upperBound
inner3: for i in (minLower ..< validLower).reversed() {
if let c = UnicodeScalar(text.character(at: i)) {
if textUrlEdgeCharacters.contains(c) {
validLower = i
} else {
break inner3
}
} else {
break inner3
}
}
let maxUpper = (i == quoteRanges.count - 1) ? fullRange.upperBound : quoteRanges[i + 1].0.lowerBound
inner3: for i in validUpper ..< maxUpper {
if let c = UnicodeScalar(text.character(at: i)) {
if textUrlEdgeCharacters.contains(c) {
validUpper = i + 1
} else {
break inner3
}
} else {
break inner3
}
}
quoteRanges[i] = (NSRange(location: validLower, length: validUpper - validLower), quoteRanges[i].1)
}
quoteRanges = quoteRanges.filter({ $0.0.length > 0 })
while quoteRanges.count > 1 {
var hadReductions = false
outer: for i in 0 ..< quoteRanges.count - 1 {
if quoteRanges[i].1 === quoteRanges[i + 1].1 {
var combine = true
inner: for j in quoteRanges[i].0.upperBound ..< quoteRanges[i + 1].0.lowerBound {
if let c = UnicodeScalar(text.character(at: j)) {
if textUrlCharacters.contains(c) {
} else {
combine = false
break inner
}
} else {
combine = false
break inner
}
}
if combine {
hadReductions = true
quoteRanges[i] = (NSRange(location: quoteRanges[i].0.lowerBound, length: quoteRanges[i + 1].0.upperBound - quoteRanges[i].0.lowerBound), quoteRanges[i].1)
quoteRanges.remove(at: i + 1)
break outer
}
}
}
if !hadReductions {
break
}
}
if quoteRanges.count > 1 {
outer: for i in (1 ..< quoteRanges.count).reversed() {
for j in 0 ..< i {
if quoteRanges[j].1 === quoteRanges[i].1 {
quoteRanges.remove(at: i)
continue outer
}
}
}
}
if !quoteRangesEqual(quoteRanges, initialQuoteRanges) {
attributedText.removeAttribute(ChatTextInputAttributes.quote, range: fullRange)
for (range, attribute) in quoteRanges {
let _ = attribute
attributedText.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: range)
}
}
}
public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set<String>, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) {
refreshChatTextInputAttributes(textView: textNode.textView, primaryTextColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, baseFontSize: baseFontSize, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider)
}
@ -534,6 +688,13 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo
resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider)
text = resultAttributedText.string as NSString
fullRange = NSRange(location: 0, length: text.length)
attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText))
refreshBlockQuotes(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange)
resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider)
if !resultAttributedText.isEqual(to: initialAttributedText) {
fullRange = NSRange(location: 0, length: textView.textStorage.length)
@ -546,6 +707,7 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo
textView.textStorage.removeAttribute(ChatTextInputAttributes.textUrl, range: fullRange)
textView.textStorage.removeAttribute(ChatTextInputAttributes.spoiler, range: fullRange)
textView.textStorage.removeAttribute(ChatTextInputAttributes.customEmoji, range: fullRange)
textView.textStorage.removeAttribute(ChatTextInputAttributes.quote, range: fullRange)
textView.textStorage.addAttribute(NSAttributedString.Key.font, value: Font.regular(baseFontSize), range: fullRange)
textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: primaryTextColor, range: fullRange)
@ -589,6 +751,13 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
textView.textStorage.addAttribute(key, value: value, range: range)
textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
} else if key == ChatTextInputAttributes.quote {
textView.textStorage.addAttribute(key, value: value, range: range)
textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: accentTextColor.withAlphaComponent(0.15), range: range)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.headIndent = 8.0
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])]
textView.textStorage.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range)
}
}
@ -690,6 +859,12 @@ public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, th
} else {
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
}
} else if key == ChatTextInputAttributes.quote {
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.15), range: range)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.headIndent = 8.0
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])]
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range)
}
}
@ -882,7 +1057,7 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu
stringOffset -= match.range(at: 2).length + match.range(at: 4).length
let substring = string.substring(with: match.range(at: 1)) + text + string.substring(with: match.range(at: 5))
result.append(NSAttributedString(string: substring, attributes: [ChatTextInputAttributes.monospace: true as NSNumber]))
result.append(NSAttributedString(string: substring, attributes: [ChatTextInputAttributes.code: true as NSNumber]))
offsetRanges.append((NSMakeRange(matchIndex + match.range(at: 1).length, text.count), 6))
}
}
@ -902,13 +1077,20 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu
} else {
let text = string.substring(with: pre)
let entity = string.substring(with: match.range(at: 7))
let substring = string.substring(with: match.range(at: 6)) + text + string.substring(with: match.range(at: 9))
var entity = string.substring(with: match.range(at: 7))
var substring = string.substring(with: match.range(at: 6)) + text + string.substring(with: match.range(at: 9))
if entity == "`" && substring.hasPrefix("``") && substring.hasSuffix("``") {
entity = "```"
substring = String(substring[substring.index(substring.startIndex, offsetBy: 2) ..< substring.index(substring.endIndex, offsetBy: -2)])
}
let textInputAttribute: NSAttributedString.Key?
switch entity {
case "`":
textInputAttribute = ChatTextInputAttributes.monospace
case "```":
textInputAttribute = ChatTextInputAttributes.code
case "**":
textInputAttribute = ChatTextInputAttributes.bold
case "__":

View File

@ -167,6 +167,10 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Spoiler))
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .CustomEmoji(stickerPack: nil, fileId: value.fileId)))
} else if key == ChatTextInputAttributes.code {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Code))
} else if key == ChatTextInputAttributes.quote {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .BlockQuote))
}
}
})

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit
import Postbox
import TelegramCore
import Display
public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [MessageTextEntity]) -> NSAttributedString {
var nsString: NSString?
@ -45,6 +46,8 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
string.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: range)
case let .CustomEmoji(_, fileId):
string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: range)
case .BlockQuote:
string.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: range)
default:
break
}
@ -52,7 +55,9 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
return string
}
public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:]) -> NSAttributedString {
public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:]) -> NSAttributedString {
let baseQuoteTintColor = baseQuoteTintColor ?? baseColor
var nsString: NSString?
let string = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor])
var skipEntity = false
@ -62,6 +67,8 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
}
var fontAttributes: [NSRange: ChatTextFontAttributes] = [:]
var nextBlockId = 0
var rangeOffset: Int = 0
for i in 0 ..< entities.count {
if skipEntity {
@ -197,13 +204,13 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
nsString = text as NSString
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand), value: nsString!.substring(with: range), range: range)
case .Code, .Pre:
case .Pre:
string.addAttribute(NSAttributedString.Key.font, value: fixedFont, range: range)
if nsString == nil {
nsString = text as NSString
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre), value: nsString!.substring(with: range), range: range)
case .BlockQuote:
case .BlockQuote, .Code:
if let fontAttribute = fontAttributes[range] {
fontAttributes[range] = fontAttribute.union(.blockQuote)
} else {
@ -211,17 +218,31 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
}
let paragraphBreak = "\n"
let paragraphRange: NSRange
if range.lowerBound == 0 {
paragraphRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)
} else if string.string[string.string.index(string.string.startIndex, offsetBy: range.lowerBound)] == "\n" {
paragraphRange = NSRange(location: range.lowerBound + 1, length: range.upperBound - range.lowerBound - 1)
} else if string.string[string.string.index(string.string.startIndex, offsetBy: range.lowerBound - 1)] == "\n" {
paragraphRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)
} else {
string.insert(NSAttributedString(string: paragraphBreak), at: range.lowerBound)
paragraphRange = NSRange(location: range.lowerBound + paragraphBreak.count, length: range.upperBound - range.lowerBound)
}
let paragraphRange = NSRange(location: range.lowerBound + paragraphBreak.count, length: range.upperBound - range.lowerBound)
string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(id: nextBlockId, title: nil, color: baseQuoteTintColor), range: paragraphRange)
nextBlockId += 1
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.headIndent = 10.0
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])]
string.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: paragraphRange)
if string.string.index(string.string.startIndex, offsetBy: paragraphRange.upperBound) != string.string.endIndex {
if string.string[string.string.index(string.string.startIndex, offsetBy: paragraphRange.upperBound)] == "\n" {
string.replaceCharacters(in: NSMakeRange(paragraphRange.upperBound, 1), with: "")
rangeOffset -= 1
}
}
string.insert(NSAttributedString(string: paragraphBreak), at: paragraphRange.upperBound)
rangeOffset += paragraphBreak.count
rangeOffset += 0
//rangeOffset += paragraphBreak.count
case .BankCard:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {
@ -268,7 +289,9 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
func addFont(ranges: [NSRange], fontAttributes: ChatTextFontAttributes) {
for range in ranges {
var font: UIFont?
if fontAttributes == [.bold, .italic] {
if fontAttributes.contains(.blockQuote) {
font = baseFont.withSize(round(baseFont.pointSize * 0.8235294117647058))
} else if fontAttributes == [.bold, .italic] {
font = boldItalicFont
} else if fontAttributes == [.bold] {
font = boldFont

View File

@ -42,4 +42,5 @@ public struct TelegramTextAttributes {
public static let BlockQuote = "TelegramBlockQuote"
public static let Pre = "TelegramPre"
public static let Spoiler = "TelegramSpoiler"
public static let Code = "TelegramCode"
}