mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 14:20:20 +00:00
Merge commit 'e39736da87299bdc568de6a9c620c668e08324be'
# Conflicts: # submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift
This commit is contained in:
@@ -15,6 +15,16 @@ private final class TextNodeStrikethrough {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class TextNodeSpoiler {
|
||||||
|
let range: NSRange
|
||||||
|
let frame: CGRect
|
||||||
|
|
||||||
|
init(range: NSRange, frame: CGRect) {
|
||||||
|
self.range = range
|
||||||
|
self.frame = frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct TextRangeRectEdge: Equatable {
|
public struct TextRangeRectEdge: Equatable {
|
||||||
public var x: CGFloat
|
public var x: CGFloat
|
||||||
public var y: CGFloat
|
public var y: CGFloat
|
||||||
@@ -33,13 +43,15 @@ private final class TextNodeLine {
|
|||||||
let range: NSRange
|
let range: NSRange
|
||||||
let isRTL: Bool
|
let isRTL: Bool
|
||||||
let strikethroughs: [TextNodeStrikethrough]
|
let strikethroughs: [TextNodeStrikethrough]
|
||||||
|
let spoilers: [TextNodeSpoiler]
|
||||||
|
|
||||||
init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough]) {
|
init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler]) {
|
||||||
self.line = line
|
self.line = line
|
||||||
self.frame = frame
|
self.frame = frame
|
||||||
self.range = range
|
self.range = range
|
||||||
self.isRTL = isRTL
|
self.isRTL = isRTL
|
||||||
self.strikethroughs = strikethroughs
|
self.strikethroughs = strikethroughs
|
||||||
|
self.spoilers = spoilers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,8 +129,9 @@ public final class TextNodeLayoutArguments {
|
|||||||
public let lineColor: UIColor?
|
public let lineColor: UIColor?
|
||||||
public let textShadowColor: UIColor?
|
public let textShadowColor: UIColor?
|
||||||
public let textStroke: (UIColor, CGFloat)?
|
public let textStroke: (UIColor, CGFloat)?
|
||||||
|
public let displaySpoilers: Bool
|
||||||
|
|
||||||
public init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, minimumNumberOfLines: Int = 0, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, verticalAlignment: TextVerticalAlignment = .top, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), lineColor: UIColor? = nil, textShadowColor: UIColor? = nil, textStroke: (UIColor, CGFloat)? = nil) {
|
public init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, minimumNumberOfLines: Int = 0, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, verticalAlignment: TextVerticalAlignment = .top, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), lineColor: UIColor? = nil, textShadowColor: UIColor? = nil, textStroke: (UIColor, CGFloat)? = nil, displaySpoilers: Bool = false) {
|
||||||
self.attributedString = attributedString
|
self.attributedString = attributedString
|
||||||
self.backgroundColor = backgroundColor
|
self.backgroundColor = backgroundColor
|
||||||
self.minimumNumberOfLines = minimumNumberOfLines
|
self.minimumNumberOfLines = minimumNumberOfLines
|
||||||
@@ -133,6 +146,7 @@ public final class TextNodeLayoutArguments {
|
|||||||
self.lineColor = lineColor
|
self.lineColor = lineColor
|
||||||
self.textShadowColor = textShadowColor
|
self.textShadowColor = textShadowColor
|
||||||
self.textStroke = textStroke
|
self.textStroke = textStroke
|
||||||
|
self.displaySpoilers = displaySpoilers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,9 +171,11 @@ public final class TextNodeLayout: NSObject {
|
|||||||
fileprivate let lineColor: UIColor?
|
fileprivate let lineColor: UIColor?
|
||||||
fileprivate let textShadowColor: UIColor?
|
fileprivate let textShadowColor: UIColor?
|
||||||
fileprivate let textStroke: (UIColor, CGFloat)?
|
fileprivate let textStroke: (UIColor, CGFloat)?
|
||||||
|
fileprivate let displaySpoilers: Bool
|
||||||
public let hasRTL: Bool
|
public let hasRTL: Bool
|
||||||
|
public let spoilers: [CGRect]
|
||||||
|
|
||||||
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, explicitAlignment: NSTextAlignment, resolvedAlignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?) {
|
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, explicitAlignment: NSTextAlignment, resolvedAlignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) {
|
||||||
self.attributedString = attributedString
|
self.attributedString = attributedString
|
||||||
self.maximumNumberOfLines = maximumNumberOfLines
|
self.maximumNumberOfLines = maximumNumberOfLines
|
||||||
self.truncationType = truncationType
|
self.truncationType = truncationType
|
||||||
@@ -180,13 +196,17 @@ public final class TextNodeLayout: NSObject {
|
|||||||
self.lineColor = lineColor
|
self.lineColor = lineColor
|
||||||
self.textShadowColor = textShadowColor
|
self.textShadowColor = textShadowColor
|
||||||
self.textStroke = textStroke
|
self.textStroke = textStroke
|
||||||
|
self.displaySpoilers = displaySpoilers
|
||||||
var hasRTL = false
|
var hasRTL = false
|
||||||
|
var spoilers: [CGRect] = []
|
||||||
for line in lines {
|
for line in lines {
|
||||||
if line.isRTL {
|
if line.isRTL {
|
||||||
hasRTL = true
|
hasRTL = true
|
||||||
}
|
}
|
||||||
|
spoilers.append(contentsOf: line.spoilers.map { $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY) })
|
||||||
}
|
}
|
||||||
self.hasRTL = hasRTL
|
self.hasRTL = hasRTL
|
||||||
|
self.spoilers = spoilers
|
||||||
}
|
}
|
||||||
|
|
||||||
public func areLinesEqual(to other: TextNodeLayout) -> Bool {
|
public func areLinesEqual(to other: TextNodeLayout) -> Bool {
|
||||||
@@ -851,7 +871,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class 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?, textStroke: (UIColor, CGFloat)?) -> TextNodeLayout {
|
private class 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?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) -> TextNodeLayout {
|
||||||
if let attributedString = attributedString {
|
if let attributedString = attributedString {
|
||||||
|
|
||||||
let stringLength = attributedString.length
|
let stringLength = attributedString.length
|
||||||
@@ -890,7 +910,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
var maybeTypesetter: CTTypesetter?
|
var maybeTypesetter: CTTypesetter?
|
||||||
maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
|
maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
|
||||||
if maybeTypesetter == nil {
|
if maybeTypesetter == nil {
|
||||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, 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, textStroke: textStroke)
|
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, 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, textStroke: textStroke, displaySpoilers: displaySpoilers)
|
||||||
}
|
}
|
||||||
|
|
||||||
let typesetter = maybeTypesetter!
|
let typesetter = maybeTypesetter!
|
||||||
@@ -931,6 +951,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
var first = true
|
var first = true
|
||||||
while true {
|
while true {
|
||||||
var strikethroughs: [TextNodeStrikethrough] = []
|
var strikethroughs: [TextNodeStrikethrough] = []
|
||||||
|
var spoilers: [TextNodeSpoiler] = []
|
||||||
|
|
||||||
var lineConstrainedWidth = constrainedSize.width
|
var lineConstrainedWidth = constrainedSize.width
|
||||||
var lineConstrainedWidthDelta: CGFloat = 0.0
|
var lineConstrainedWidthDelta: CGFloat = 0.0
|
||||||
@@ -996,7 +1017,32 @@ public class TextNode: ASDisplayNode {
|
|||||||
|
|
||||||
var headIndent: CGFloat = 0.0
|
var headIndent: CGFloat = 0.0
|
||||||
attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in
|
attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in
|
||||||
if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
|
if let _ = attributes[NSAttributedString.Key.init(rawValue: "TelegramSpoiler")] {
|
||||||
|
var ascent: CGFloat = 0.0
|
||||||
|
var descent: CGFloat = 0.0
|
||||||
|
CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil)
|
||||||
|
|
||||||
|
(attributedString.string as NSString).enumerateSubstrings(in: range, options: .byWords) { _, range, _, _ in
|
||||||
|
let startIndex = range.location
|
||||||
|
let endIndex = range.location + range.length
|
||||||
|
|
||||||
|
var secondaryLeftOffset: CGFloat = 0.0
|
||||||
|
let rawLeftOffset = CTLineGetOffsetForStringIndex(coreTextLine, startIndex, &secondaryLeftOffset)
|
||||||
|
var leftOffset = ceil(rawLeftOffset)
|
||||||
|
if !rawLeftOffset.isEqual(to: secondaryLeftOffset) {
|
||||||
|
leftOffset = ceil(secondaryLeftOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondaryRightOffset: CGFloat = 0.0
|
||||||
|
let rawRighOffset = CTLineGetOffsetForStringIndex(coreTextLine, endIndex, &secondaryRightOffset)
|
||||||
|
var rightOffset = ceil(rawRighOffset)
|
||||||
|
if !rawRighOffset.isEqual(to: secondaryRightOffset) {
|
||||||
|
rightOffset = ceil(secondaryRightOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
spoilers.append(TextNodeSpoiler(range: range, frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent)))
|
||||||
|
}
|
||||||
|
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
|
||||||
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
|
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
|
||||||
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
|
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
|
||||||
let x = lowerX < upperX ? lowerX : upperX
|
let x = lowerX < upperX ? lowerX : upperX
|
||||||
@@ -1025,7 +1071,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs))
|
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers))
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
if lineCharacterCount > 0 {
|
if lineCharacterCount > 0 {
|
||||||
@@ -1048,7 +1094,32 @@ public class TextNode: ASDisplayNode {
|
|||||||
|
|
||||||
var headIndent: CGFloat = 0.0
|
var headIndent: CGFloat = 0.0
|
||||||
attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in
|
attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in
|
||||||
if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
|
if let _ = attributes[NSAttributedString.Key.init(rawValue: "TelegramSpoiler")] {
|
||||||
|
var ascent: CGFloat = 0.0
|
||||||
|
var descent: CGFloat = 0.0
|
||||||
|
CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil)
|
||||||
|
|
||||||
|
(attributedString.string as NSString).enumerateSubstrings(in: range, options: .byWords) { _, range, _, _ in
|
||||||
|
let startIndex = range.location
|
||||||
|
let endIndex = range.location + range.length
|
||||||
|
|
||||||
|
var secondaryLeftOffset: CGFloat = 0.0
|
||||||
|
let rawLeftOffset = CTLineGetOffsetForStringIndex(coreTextLine, startIndex, &secondaryLeftOffset)
|
||||||
|
var leftOffset = ceil(rawLeftOffset)
|
||||||
|
if !rawLeftOffset.isEqual(to: secondaryLeftOffset) {
|
||||||
|
leftOffset = ceil(secondaryLeftOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondaryRightOffset: CGFloat = 0.0
|
||||||
|
let rawRighOffset = CTLineGetOffsetForStringIndex(coreTextLine, endIndex, &secondaryRightOffset)
|
||||||
|
var rightOffset = ceil(rawRighOffset)
|
||||||
|
if !rawRighOffset.isEqual(to: secondaryRightOffset) {
|
||||||
|
rightOffset = ceil(secondaryRightOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
spoilers.append(TextNodeSpoiler(range: range, frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent)))
|
||||||
|
}
|
||||||
|
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
|
||||||
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
|
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
|
||||||
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
|
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
|
||||||
let x = lowerX < upperX ? lowerX : upperX
|
let x = lowerX < upperX ? lowerX : upperX
|
||||||
@@ -1076,7 +1147,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs))
|
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers))
|
||||||
} else {
|
} else {
|
||||||
if !lines.isEmpty {
|
if !lines.isEmpty {
|
||||||
layoutSize.height += fontLineSpacing
|
layoutSize.height += fontLineSpacing
|
||||||
@@ -1109,9 +1180,9 @@ public 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, textStroke: textStroke)
|
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, textStroke: textStroke, displaySpoilers: displaySpoilers)
|
||||||
} else {
|
} 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, textStroke: textStroke)
|
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, textStroke: textStroke, displaySpoilers: displaySpoilers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,6 +1208,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
context.setAllowsFontSubpixelQuantization(true)
|
context.setAllowsFontSubpixelQuantization(true)
|
||||||
context.setShouldSubpixelQuantizeFonts(true)
|
context.setShouldSubpixelQuantizeFonts(true)
|
||||||
|
|
||||||
|
var clearRects: [CGRect] = []
|
||||||
if let layout = parameters as? TextNodeLayout {
|
if let layout = parameters as? TextNodeLayout {
|
||||||
if !isRasterizing || layout.backgroundColor != nil {
|
if !isRasterizing || layout.backgroundColor != nil {
|
||||||
context.setBlendMode(.copy)
|
context.setBlendMode(.copy)
|
||||||
@@ -1189,6 +1261,15 @@ public class TextNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
context.textPosition = CGPoint(x: lineFrame.minX, y: lineFrame.minY)
|
context.textPosition = CGPoint(x: lineFrame.minX, y: lineFrame.minY)
|
||||||
|
|
||||||
|
if layout.displaySpoilers && !line.spoilers.isEmpty {
|
||||||
|
context.saveGState()
|
||||||
|
var clipRects: [CGRect] = []
|
||||||
|
for spoiler in line.spoilers {
|
||||||
|
clipRects.append(spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY))
|
||||||
|
}
|
||||||
|
context.clip(to: clipRects)
|
||||||
|
}
|
||||||
|
|
||||||
let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray
|
let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray
|
||||||
if glyphRuns.count != 0 {
|
if glyphRuns.count != 0 {
|
||||||
for run in glyphRuns {
|
for run in glyphRuns {
|
||||||
@@ -1213,6 +1294,16 @@ public class TextNode: ASDisplayNode {
|
|||||||
context.fill(CGRect(x: frame.minX, y: frame.minY - 5.0, width: frame.width, height: 1.0))
|
context.fill(CGRect(x: frame.minX, y: frame.minY - 5.0, width: frame.width, height: 1.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !line.spoilers.isEmpty {
|
||||||
|
if layout.displaySpoilers {
|
||||||
|
context.restoreGState()
|
||||||
|
} else {
|
||||||
|
for spoiler in line.spoilers {
|
||||||
|
clearRects.append(spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var blockQuoteFrames: [CGRect] = []
|
var blockQuoteFrames: [CGRect] = []
|
||||||
@@ -1248,6 +1339,10 @@ public class TextNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.setBlendMode(.normal)
|
context.setBlendMode(.normal)
|
||||||
|
|
||||||
|
for rect in clearRects {
|
||||||
|
context.clear(rect)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func asyncLayout(_ maybeNode: TextNode?) -> (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) {
|
public static func asyncLayout(_ maybeNode: TextNode?) -> (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) {
|
||||||
@@ -1282,11 +1377,11 @@ public class TextNode: ASDisplayNode {
|
|||||||
if stringMatch {
|
if stringMatch {
|
||||||
layout = existingLayout
|
layout = existingLayout
|
||||||
} else {
|
} else {
|
||||||
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke)
|
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers)
|
||||||
updated = true
|
updated = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke)
|
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers)
|
||||||
updated = true
|
updated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
submodules/InvisibleInkDustNode/BUILD
Normal file
21
submodules/InvisibleInkDustNode/BUILD
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "InvisibleInkDustNode",
|
||||||
|
module_name = "InvisibleInkDustNode",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||||
|
"//submodules/Display:Display",
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||||
|
"//submodules/AppBundle:AppBundle",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import UIKit.UIGestureRecognizerSubclass
|
||||||
|
import SwiftSignalKit
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import AppBundle
|
||||||
|
|
||||||
|
private func createEmitterBehavior(type: String) -> NSObject {
|
||||||
|
let selector = ["behaviorWith", "Type:"].joined(separator: "")
|
||||||
|
let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type
|
||||||
|
let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))!
|
||||||
|
let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self)
|
||||||
|
return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private let textMaskImage: UIImage = {
|
||||||
|
return generateImage(CGSize(width: 60.0, height: 60.0), rotatedContext: { size, context in
|
||||||
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||||
|
context.clear(bounds)
|
||||||
|
|
||||||
|
var locations: [CGFloat] = [0.0, 0.7, 0.95, 1.0]
|
||||||
|
let colors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 1.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor]
|
||||||
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||||
|
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||||
|
|
||||||
|
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||||
|
context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: size.width / 2.0, options: .drawsAfterEndLocation)
|
||||||
|
})!
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let emitterMaskImage: UIImage = {
|
||||||
|
return generateImage(CGSize(width: 120.0, height: 120.0), rotatedContext: { size, context in
|
||||||
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||||
|
context.clear(bounds)
|
||||||
|
|
||||||
|
var locations: [CGFloat] = [0.0, 0.7, 0.95, 1.0]
|
||||||
|
let colors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor]
|
||||||
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||||
|
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||||
|
|
||||||
|
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||||
|
context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: size.width / 10.0, options: .drawsAfterEndLocation)
|
||||||
|
})!
|
||||||
|
}()
|
||||||
|
|
||||||
|
public class InvisibleInkDustNode: ASDisplayNode {
|
||||||
|
private var currentParams: (size: CGSize, color: UIColor, rects: [CGRect])?
|
||||||
|
|
||||||
|
private weak var textNode: TextNode?
|
||||||
|
private let textMaskNode: ASDisplayNode
|
||||||
|
private let textSpotNode: ASImageNode
|
||||||
|
|
||||||
|
private var emitterNode: ASDisplayNode
|
||||||
|
private var emitter: CAEmitterCell?
|
||||||
|
private var emitterLayer: CAEmitterLayer?
|
||||||
|
private let emitterMaskNode: ASDisplayNode
|
||||||
|
private let emitterSpotNode: ASImageNode
|
||||||
|
private let emitterMaskFillNode: ASDisplayNode
|
||||||
|
|
||||||
|
public var isRevealedUpdated: (Bool) -> Void = { _ in }
|
||||||
|
|
||||||
|
public init(textNode: TextNode) {
|
||||||
|
self.textNode = textNode
|
||||||
|
|
||||||
|
self.emitterNode = ASDisplayNode()
|
||||||
|
self.emitterNode.clipsToBounds = true
|
||||||
|
|
||||||
|
self.textMaskNode = ASDisplayNode()
|
||||||
|
self.textSpotNode = ASImageNode()
|
||||||
|
let img = textMaskImage
|
||||||
|
self.textSpotNode.image = img
|
||||||
|
|
||||||
|
self.emitterMaskNode = ASDisplayNode()
|
||||||
|
self.emitterSpotNode = ASImageNode()
|
||||||
|
let simg = emitterMaskImage
|
||||||
|
self.emitterSpotNode.image = simg
|
||||||
|
|
||||||
|
self.emitterMaskFillNode = ASDisplayNode()
|
||||||
|
self.emitterMaskFillNode.backgroundColor = .white
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.emitterNode)
|
||||||
|
|
||||||
|
self.textMaskNode.addSubnode(self.textSpotNode)
|
||||||
|
self.emitterMaskNode.addSubnode(self.emitterSpotNode)
|
||||||
|
self.emitterMaskNode.addSubnode(self.emitterMaskFillNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
let emitter = CAEmitterCell()
|
||||||
|
emitter.contents = UIImage(bundleImageName: "Components/TextSpeckle")?.cgImage
|
||||||
|
emitter.setValue(1.8, forKey: "contentsScale")
|
||||||
|
emitter.emissionRange = .pi * 2.0
|
||||||
|
emitter.setValue(3.0, forKey: "mass")
|
||||||
|
emitter.setValue(2.0, forKey: "massRange")
|
||||||
|
emitter.lifetime = 1.0
|
||||||
|
emitter.scale = 0.5
|
||||||
|
emitter.velocityRange = 20.0
|
||||||
|
emitter.name = "dustCell"
|
||||||
|
emitter.setValue("point", forKey: "particleType")
|
||||||
|
emitter.color = UIColor.white.withAlphaComponent(0.0).cgColor
|
||||||
|
emitter.alphaRange = 1.0
|
||||||
|
self.emitter = emitter
|
||||||
|
|
||||||
|
let fingerAttractor = createEmitterBehavior(type: "simpleAttractor")
|
||||||
|
fingerAttractor.setValue("fingerAttractor", forKey: "name")
|
||||||
|
|
||||||
|
let alphaBehavior = createEmitterBehavior(type: "valueOverLife")
|
||||||
|
alphaBehavior.setValue("alphaBehavior", forKey: "name")
|
||||||
|
alphaBehavior.setValue("color.alpha", forKey: "keyPath")
|
||||||
|
alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values")
|
||||||
|
alphaBehavior.setValue(true, forKey: "additive")
|
||||||
|
|
||||||
|
let behaviors = [fingerAttractor, alphaBehavior]
|
||||||
|
|
||||||
|
let emitterLayer = CAEmitterLayer()
|
||||||
|
emitterLayer.masksToBounds = true
|
||||||
|
emitterLayer.allowsGroupOpacity = true
|
||||||
|
emitterLayer.lifetime = 1
|
||||||
|
emitterLayer.emitterCells = [emitter]
|
||||||
|
emitterLayer.setValue(behaviors, forKey: "emitterBehaviors")
|
||||||
|
emitterLayer.emitterPosition = CGPoint(x: 0, y: 0)
|
||||||
|
emitterLayer.seed = arc4random()
|
||||||
|
emitterLayer.setValue("rectangles", forKey: "emitterShape")
|
||||||
|
emitterLayer.emitterSize = CGSize(width: 1, height: 1)
|
||||||
|
emitterLayer.setValue(0.0322, forKey: "updateInterval")
|
||||||
|
|
||||||
|
emitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.fingerAttractor.stiffness")
|
||||||
|
emitterLayer.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
|
||||||
|
|
||||||
|
self.emitterLayer = emitterLayer
|
||||||
|
|
||||||
|
self.emitterNode.layer.addSublayer(emitterLayer)
|
||||||
|
|
||||||
|
self.updateEmitter()
|
||||||
|
|
||||||
|
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap(_:))))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var revealed = false
|
||||||
|
@objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||||
|
guard let (size, _, _) = self.currentParams, !self.revealed else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.revealed = true
|
||||||
|
|
||||||
|
let position = gestureRecognizer.location(in: self.view)
|
||||||
|
self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
|
||||||
|
self.emitterLayer?.setValue(position, forKeyPath: "emitterBehaviors.fingerAttractor.position")
|
||||||
|
|
||||||
|
Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) {
|
||||||
|
self.textNode?.view.mask = self.textMaskNode.view
|
||||||
|
self.textNode?.alpha = 1.0
|
||||||
|
|
||||||
|
let radius = max(size.width, size.height)
|
||||||
|
self.textSpotNode.frame = CGRect(x: position.x - radius / 2.0, y: position.y - radius / 2.0, width: radius, height: radius)
|
||||||
|
|
||||||
|
self.textSpotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||||
|
self.textSpotNode.layer.animateScale(from: 0.1, to: 3.5, duration: 0.71, removeOnCompletion: false, completion: { [weak self] _ in
|
||||||
|
self?.textNode?.view.mask = nil
|
||||||
|
})
|
||||||
|
|
||||||
|
self.emitterNode.view.mask = self.emitterMaskNode.view
|
||||||
|
let emitterSide = radius * 5.0
|
||||||
|
self.emitterSpotNode.frame = CGRect(x: position.x - emitterSide / 2.0, y: position.y - emitterSide / 2.0, width: emitterSide, height: emitterSide)
|
||||||
|
self.emitterSpotNode.layer.animateScale(from: 0.1, to: 3.0, duration: 0.71, removeOnCompletion: false, completion: { [weak self] _ in
|
||||||
|
self?.alpha = 0.0
|
||||||
|
self?.emitterNode.view.mask = nil
|
||||||
|
})
|
||||||
|
self.emitterMaskFillNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||||
|
|
||||||
|
// let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear)
|
||||||
|
// transition.updateAlpha(node: self, alpha: 0.0)
|
||||||
|
|
||||||
|
self.isRevealedUpdated(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) {
|
||||||
|
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
|
||||||
|
self.textSpotNode.layer.removeAllAnimations()
|
||||||
|
|
||||||
|
self.emitterSpotNode.layer.removeAllAnimations()
|
||||||
|
self.emitterMaskFillNode.layer.removeAllAnimations()
|
||||||
|
}
|
||||||
|
|
||||||
|
Queue.mainQueue().after(4.0 * UIView.animationDurationFactor()) {
|
||||||
|
self.revealed = false
|
||||||
|
self.isRevealedUpdated(false)
|
||||||
|
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear)
|
||||||
|
transition.updateAlpha(node: self, alpha: 1.0)
|
||||||
|
if let textNode = self.textNode {
|
||||||
|
transition.updateAlpha(node: textNode, alpha: 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateEmitter() {
|
||||||
|
guard let (size, color, rects) = self.currentParams else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.emitter?.color = color.cgColor
|
||||||
|
self.emitterLayer?.setValue(rects, forKey: "emitterRects")
|
||||||
|
self.emitterLayer?.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
|
||||||
|
let radius = max(size.width, size.height)
|
||||||
|
self.emitterLayer?.setValue(max(size.width, size.height), forKeyPath: "emitterBehaviors.fingerAttractor.radius")
|
||||||
|
self.emitterLayer?.setValue(radius * -0.5, forKeyPath: "emitterBehaviors.fingerAttractor.falloff")
|
||||||
|
|
||||||
|
var square: Float = 0.0
|
||||||
|
for rect in rects {
|
||||||
|
square += Float(rect.width * rect.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.emitter?.birthRate = square * 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
public func update(size: CGSize, color: UIColor, rects: [CGRect]) {
|
||||||
|
self.currentParams = (size, color, rects)
|
||||||
|
|
||||||
|
self.emitterNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
self.emitterMaskNode.frame = self.emitterNode.bounds
|
||||||
|
self.emitterMaskFillNode.frame = self.emitterNode.bounds
|
||||||
|
self.textMaskNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: size)
|
||||||
|
|
||||||
|
if self.isNodeLoaded {
|
||||||
|
self.updateEmitter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||||
|
if let (_, _, rects) = self.currentParams {
|
||||||
|
for rect in rects {
|
||||||
|
if rect.contains(point) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -444,7 +444,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
|
|||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.presentationDataValue.set(.single(presentationData))
|
self.presentationDataValue.set(.single(presentationData))
|
||||||
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
||||||
self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.chatList.backgroundColor, direction: true)
|
self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true)
|
||||||
self.searchDisplayController?.updatePresentationData(presentationData)
|
self.searchDisplayController?.updatePresentationData(presentationData)
|
||||||
self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
||||||
self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ swift_library(
|
|||||||
"//submodules/DirectMediaImageCache:DirectMediaImageCache",
|
"//submodules/DirectMediaImageCache:DirectMediaImageCache",
|
||||||
"//submodules/CodeInputView:CodeInputView",
|
"//submodules/CodeInputView:CodeInputView",
|
||||||
"//submodules/Components/ReactionButtonListComponent:ReactionButtonListComponent",
|
"//submodules/Components/ReactionButtonListComponent:ReactionButtonListComponent",
|
||||||
|
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
|
||||||
] + select({
|
] + select({
|
||||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||||
|
|||||||
21
submodules/TelegramUI/Images.xcassets/Components/TextSpeckle.imageset/Contents.json
vendored
Normal file
21
submodules/TelegramUI/Images.xcassets/Components/TextSpeckle.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "textSpeckle_Normal.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
submodules/TelegramUI/Images.xcassets/Components/TextSpeckle.imageset/textSpeckle_Normal.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Components/TextSpeckle.imageset/textSpeckle_Normal.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
@@ -8,6 +8,7 @@ import TextFormat
|
|||||||
import UrlEscaping
|
import UrlEscaping
|
||||||
import TelegramUniversalVideoContent
|
import TelegramUniversalVideoContent
|
||||||
import TextSelectionNode
|
import TextSelectionNode
|
||||||
|
import InvisibleInkDustNode
|
||||||
|
|
||||||
private final class CachedChatMessageText {
|
private final class CachedChatMessageText {
|
||||||
let text: String
|
let text: String
|
||||||
@@ -37,6 +38,9 @@ private final class CachedChatMessageText {
|
|||||||
|
|
||||||
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||||
private let textNode: TextNode
|
private let textNode: TextNode
|
||||||
|
private var spoilerTextNode: TextNode?
|
||||||
|
private var dustNode: InvisibleInkDustNode?
|
||||||
|
|
||||||
private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode
|
private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode
|
||||||
private let statusNode: ChatMessageDateAndStatusNode
|
private let statusNode: ChatMessageDateAndStatusNode
|
||||||
private var linkHighlightingNode: LinkHighlightingNode?
|
private var linkHighlightingNode: LinkHighlightingNode?
|
||||||
@@ -80,6 +84,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||||
let textLayout = TextNode.asyncLayout(self.textNode)
|
let textLayout = TextNode.asyncLayout(self.textNode)
|
||||||
|
let spoilerTextLayout = TextNode.asyncLayout(self.spoilerTextNode)
|
||||||
let statusLayout = self.statusNode.asyncLayout()
|
let statusLayout = self.statusNode.asyncLayout()
|
||||||
|
|
||||||
let currentCachedChatMessageText = self.cachedChatMessageText
|
let currentCachedChatMessageText = self.cachedChatMessageText
|
||||||
@@ -273,7 +278,14 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor))
|
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor))
|
||||||
|
|
||||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
|
let spoilerTextLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||||
|
if !textLayout.spoilers.isEmpty {
|
||||||
|
spoilerTextLayoutAndApply = spoilerTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor, displaySpoilers: true))
|
||||||
|
} else {
|
||||||
|
spoilerTextLayoutAndApply = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))?
|
||||||
if let statusType = statusType {
|
if let statusType = statusType {
|
||||||
var isReplyThread = false
|
var isReplyThread = false
|
||||||
if case .replyThread = item.chatLocation {
|
if case .replyThread = item.chatLocation {
|
||||||
@@ -353,8 +365,39 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
strongSelf.textNode.displaysAsynchronously = !item.presentationData.isPreview && !item.presentationData.theme.theme.forceSync
|
strongSelf.textNode.displaysAsynchronously = !item.presentationData.isPreview && !item.presentationData.theme.theme.forceSync
|
||||||
let _ = textApply()
|
let _ = textApply()
|
||||||
|
|
||||||
animation.animator.updateFrame(layer: strongSelf.textNode.layer, frame: textFrame, completion: nil)
|
animation.animator.updateFrame(layer: strongSelf.textNode.layer, frame: textFrame, completion: nil)
|
||||||
|
|
||||||
|
if let (_, spoilerTextApply) = spoilerTextLayoutAndApply {
|
||||||
|
let spoilerTextNode = spoilerTextApply()
|
||||||
|
if strongSelf.spoilerTextNode == nil {
|
||||||
|
spoilerTextNode.isUserInteractionEnabled = false
|
||||||
|
spoilerTextNode.contentMode = .topLeft
|
||||||
|
spoilerTextNode.contentsScale = UIScreenScale
|
||||||
|
spoilerTextNode.displaysAsynchronously = false
|
||||||
|
strongSelf.insertSubnode(spoilerTextNode, aboveSubnode: strongSelf.textAccessibilityOverlayNode)
|
||||||
|
|
||||||
|
strongSelf.spoilerTextNode = spoilerTextNode
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.spoilerTextNode?.frame = textFrame
|
||||||
|
strongSelf.spoilerTextNode?.isHidden = false
|
||||||
|
strongSelf.spoilerTextNode?.alpha = 0.0
|
||||||
|
|
||||||
|
let dustNode: InvisibleInkDustNode
|
||||||
|
if let current = strongSelf.dustNode {
|
||||||
|
dustNode = current
|
||||||
|
} else {
|
||||||
|
dustNode = InvisibleInkDustNode(textNode: spoilerTextNode)
|
||||||
|
strongSelf.dustNode = dustNode
|
||||||
|
strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode)
|
||||||
|
}
|
||||||
|
dustNode.update(size: textFrame.size, color: messageTheme.primaryTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) })
|
||||||
|
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
||||||
|
} else if let spoilerTextNode = strongSelf.spoilerTextNode {
|
||||||
|
strongSelf.spoilerTextNode = nil
|
||||||
|
spoilerTextNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
|
||||||
if let textSelectionNode = strongSelf.textSelectionNode {
|
if let textSelectionNode = strongSelf.textSelectionNode {
|
||||||
let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size
|
let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size
|
||||||
textSelectionNode.frame = textFrame
|
textSelectionNode.frame = textFrame
|
||||||
|
|||||||
@@ -137,7 +137,8 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
|
|||||||
}
|
}
|
||||||
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention), value: nsString!.substring(with: range), range: range)
|
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention), value: nsString!.substring(with: range), range: range)
|
||||||
case .Strikethrough:
|
case .Strikethrough:
|
||||||
string.addAttribute(NSAttributedString.Key.strikethroughStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
|
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler), value: true as NSNumber, range: range)
|
||||||
|
// string.addAttribute(NSAttributedString.Key.strikethroughStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
|
||||||
case .Underline:
|
case .Underline:
|
||||||
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
|
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
|
||||||
case let .TextMention(peerId):
|
case let .TextMention(peerId):
|
||||||
|
|||||||
@@ -41,4 +41,5 @@ public struct TelegramTextAttributes {
|
|||||||
public static let Timecode = "TelegramTimecode"
|
public static let Timecode = "TelegramTimecode"
|
||||||
public static let BlockQuote = "TelegramBlockQuote"
|
public static let BlockQuote = "TelegramBlockQuote"
|
||||||
public static let Pre = "TelegramPre"
|
public static let Pre = "TelegramPre"
|
||||||
|
public static let Spoiler = "TelegramSpoiler"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user