Collapsible quote improvements

This commit is contained in:
Isaac 2024-05-31 18:00:44 +04:00
parent b90e3563a3
commit 97bbf3ee0d
5 changed files with 322 additions and 246 deletions

View File

@ -550,7 +550,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
attributedText = updatedString attributedText = updatedString
} }
var customTruncationToken: NSAttributedString? var customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)?
var maximumNumberOfLines: Int = 0 var maximumNumberOfLines: Int = 0
if item.presentationData.isPreview { if item.presentationData.isPreview {
if item.message.groupingKey != nil { if item.message.groupingKey != nil {
@ -573,10 +573,17 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
maximumNumberOfLines = 12 maximumNumberOfLines = 12
} }
let truncationToken = NSMutableAttributedString() let truncationTokenText = item.presentationData.strings.Conversation_ReadMore
truncationToken.append(NSAttributedString(string: "\u{2026} ", font: textFont, textColor: messageTheme.primaryTextColor)) customTruncationToken = { baseFont, isQuote in
truncationToken.append(NSAttributedString(string: item.presentationData.strings.Conversation_ReadMore, font: textFont, textColor: messageTheme.accentTextColor)) let truncationToken = NSMutableAttributedString()
customTruncationToken = truncationToken if isQuote {
truncationToken.append(NSAttributedString(string: "\u{2026}", font: Font.regular(baseFont.pointSize), textColor: messageTheme.primaryTextColor))
} else {
truncationToken.append(NSAttributedString(string: "\u{2026} ", font: Font.regular(baseFont.pointSize), textColor: messageTheme.primaryTextColor))
truncationToken.append(NSAttributedString(string: truncationTokenText, font: Font.regular(baseFont.pointSize), textColor: messageTheme.accentTextColor))
}
return truncationToken
}
} }
let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0) let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0)
@ -1416,7 +1423,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return return
} }
self.displayContentsUnderSpoilers = (value, location) self.displayContentsUnderSpoilers = (value, location)
//self.displayContentsUnderSpoilers.location = nil
if let item = self.item { if let item = self.item {
item.controllerInteraction.requestMessageUpdate(item.message.id, false) item.controllerInteraction.requestMessageUpdate(item.message.id, false)
} }

View File

@ -35,12 +35,19 @@ private func generateBlockMaskImage() -> UIImage {
let colorSpace = CGColorSpaceCreateDeviceRGB() let colorSpace = CGColorSpaceCreateDeviceRGB()
var locations: [CGFloat] = [0.0, 0.5, 1.0] var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.cgColor] var colors: [CGColor] = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.cgColor]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! var gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.setBlendMode(.copy) context.setBlendMode(.copy)
context.drawRadialGradient(gradient, startCenter: CGPoint(x: size.width - 20.0, y: size.height), startRadius: 0.0, endCenter: CGPoint(x: size.width - 20.0, y: size.height), endRadius: 34.0, options: CGGradientDrawingOptions()) context.drawRadialGradient(gradient, startCenter: CGPoint(x: size.width - 20.0, y: size.height), startRadius: 0.0, endCenter: CGPoint(x: size.width - 20.0, y: size.height), endRadius: 34.0, options: CGGradientDrawingOptions())
locations = [0.0, 0.4, 1.0]
colors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.cgColor]
gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.setBlendMode(.destinationIn)
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: size.height), end: CGPoint(x: 0.0, y: size.height - 8.0), options: CGGradientDrawingOptions())
})!.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: size.height - 1.0, right: size.width - 1.0), resizingMode: .stretch) })!.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: size.height - 1.0, right: size.width - 1.0), resizingMode: .stretch)
} }
@ -96,7 +103,9 @@ private final class InteractiveTextNodeAttachment {
private final class InteractiveTextNodeLine { private final class InteractiveTextNodeLine {
let line: CTLine let line: CTLine
let constrainedWidth: CGFloat
var frame: CGRect var frame: CGRect
let intrinsicWidth: CGFloat
let ascent: CGFloat let ascent: CGFloat
let descent: CGFloat let descent: CGFloat
let range: NSRange? let range: NSRange?
@ -108,9 +117,11 @@ private final class InteractiveTextNodeLine {
var attachments: [InteractiveTextNodeAttachment] var attachments: [InteractiveTextNodeAttachment]
let additionalTrailingLine: (CTLine, Double)? let additionalTrailingLine: (CTLine, Double)?
init(line: CTLine, frame: CGRect, ascent: CGFloat, descent: CGFloat, range: NSRange?, isRTL: Bool, strikethroughs: [InteractiveTextNodeStrikethrough], spoilers: [InteractiveTextNodeSpoiler], spoilerWords: [InteractiveTextNodeSpoiler], embeddedItems: [InteractiveTextNodeEmbeddedItem], attachments: [InteractiveTextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { init(line: CTLine, constrainedWidth: CGFloat, frame: CGRect, intrinsicWidth: CGFloat, ascent: CGFloat, descent: CGFloat, range: NSRange?, isRTL: Bool, strikethroughs: [InteractiveTextNodeStrikethrough], spoilers: [InteractiveTextNodeSpoiler], spoilerWords: [InteractiveTextNodeSpoiler], embeddedItems: [InteractiveTextNodeEmbeddedItem], attachments: [InteractiveTextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) {
self.line = line self.line = line
self.constrainedWidth = constrainedWidth
self.frame = frame self.frame = frame
self.intrinsicWidth = intrinsicWidth
self.ascent = ascent self.ascent = ascent
self.descent = descent self.descent = descent
self.range = range self.range = range
@ -269,7 +280,7 @@ public final class InteractiveTextNodeLayoutArguments {
public let textShadowBlur: CGFloat? public let textShadowBlur: CGFloat?
public let textStroke: (UIColor, CGFloat)? public let textStroke: (UIColor, CGFloat)?
public let displayContentsUnderSpoilers: Bool public let displayContentsUnderSpoilers: Bool
public let customTruncationToken: NSAttributedString? public let customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)?
public let expandedBlocks: Set<Int> public let expandedBlocks: Set<Int>
public init( public init(
@ -289,7 +300,7 @@ public final class InteractiveTextNodeLayoutArguments {
textShadowBlur: CGFloat? = nil, textShadowBlur: CGFloat? = nil,
textStroke: (UIColor, CGFloat)? = nil, textStroke: (UIColor, CGFloat)? = nil,
displayContentsUnderSpoilers: Bool = false, displayContentsUnderSpoilers: Bool = false,
customTruncationToken: NSAttributedString? = nil, customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)? = nil,
expandedBlocks: Set<Int> = Set() expandedBlocks: Set<Int> = Set()
) { ) {
self.attributedString = attributedString self.attributedString = attributedString
@ -1256,7 +1267,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
textShadowBlur: CGFloat?, textShadowBlur: CGFloat?,
textStroke: (UIColor, CGFloat)?, textStroke: (UIColor, CGFloat)?,
displayContentsUnderSpoilers: Bool, displayContentsUnderSpoilers: Bool,
customTruncationToken: NSAttributedString?, customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)?,
expandedBlocks: Set<Int> expandedBlocks: Set<Int>
) -> InteractiveTextNodeLayout { ) -> InteractiveTextNodeLayout {
let blockQuoteLeftInset: CGFloat = 9.0 let blockQuoteLeftInset: CGFloat = 9.0
@ -1348,7 +1359,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
} }
} }
struct CalculatedSegment { class CalculatedSegment {
var titleLine: InteractiveTextNodeLine? var titleLine: InteractiveTextNodeLine?
var lines: [InteractiveTextNodeLine] = [] var lines: [InteractiveTextNodeLine] = []
var tintColor: UIColor? var tintColor: UIColor?
@ -1356,16 +1367,18 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
var tertiaryTintColor: UIColor? var tertiaryTintColor: UIColor?
var blockQuote: TextNodeBlockQuoteData? var blockQuote: TextNodeBlockQuoteData?
var additionalWidth: CGFloat = 0.0 var additionalWidth: CGFloat = 0.0
init() {
}
} }
var calculatedSegments: [CalculatedSegment] = [] var calculatedSegments: [CalculatedSegment] = []
var remainingLines = maximumNumberOfLines <= 0 ? Int.max : maximumNumberOfLines
for segment in stringSegments { for segment in stringSegments {
var calculatedSegment = CalculatedSegment() if remainingLines <= 0 {
calculatedSegment.blockQuote = segment.blockQuote break
calculatedSegment.tintColor = segment.tintColor }
calculatedSegment.secondaryTintColor = segment.secondaryTintColor
calculatedSegment.tertiaryTintColor = segment.tertiaryTintColor
let rawSubstring = segment.substring.string as NSString let rawSubstring = segment.substring.string as NSString
let substringLength = rawSubstring.length let substringLength = rawSubstring.length
@ -1376,6 +1389,12 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
var currentLineStartIndex = segment.firstCharacterOffset var currentLineStartIndex = segment.firstCharacterOffset
let segmentEndIndex = segment.firstCharacterOffset + substringLength let segmentEndIndex = segment.firstCharacterOffset + substringLength
let calculatedSegment = CalculatedSegment()
calculatedSegment.blockQuote = segment.blockQuote
calculatedSegment.tintColor = segment.tintColor
calculatedSegment.secondaryTintColor = segment.secondaryTintColor
calculatedSegment.tertiaryTintColor = segment.tertiaryTintColor
var constrainedSegmentWidth = constrainedSize.width var constrainedSegmentWidth = constrainedSize.width
var additionalOffsetX: CGFloat = 0.0 var additionalOffsetX: CGFloat = 0.0
if segment.blockQuote != nil { if segment.blockQuote != nil {
@ -1398,13 +1417,16 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
if let title = segment.title { if let title = segment.title {
let rawTitleLine = CTLineCreateWithAttributedString(title) let rawTitleLine = CTLineCreateWithAttributedString(title)
if let titleLine = CTLineCreateTruncatedLine(rawTitleLine, constrainedSegmentWidth - additionalSegmentRightInset, .end, nil) { let constrainedLineWidth = constrainedSegmentWidth - additionalSegmentRightInset
if let titleLine = CTLineCreateTruncatedLine(rawTitleLine, constrainedLineWidth, .end, nil) {
var lineAscent: CGFloat = 0.0 var lineAscent: CGFloat = 0.0
var lineDescent: CGFloat = 0.0 var lineDescent: CGFloat = 0.0
let lineWidth = CTLineGetTypographicBounds(titleLine, &lineAscent, &lineDescent, nil) let lineWidth = CTLineGetTypographicBounds(titleLine, &lineAscent, &lineDescent, nil)
calculatedSegment.titleLine = InteractiveTextNodeLine( calculatedSegment.titleLine = InteractiveTextNodeLine(
line: titleLine, line: titleLine,
constrainedWidth: constrainedLineWidth,
frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)), frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)),
intrinsicWidth: lineWidth,
ascent: lineAscent, ascent: lineAscent,
descent: lineDescent, descent: lineDescent,
range: nil, range: nil,
@ -1421,7 +1443,8 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
} }
while true { while true {
let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, currentLineStartIndex, constrainedSegmentWidth - additionalSegmentRightInset) let constrainedLineWidth = constrainedSegmentWidth - additionalSegmentRightInset
let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, currentLineStartIndex, constrainedLineWidth)
if lineCharacterCount != 0 { if lineCharacterCount != 0 {
let line = CTTypesetterCreateLine(typesetter, CFRange(location: currentLineStartIndex, length: lineCharacterCount)) let line = CTTypesetterCreateLine(typesetter, CFRange(location: currentLineStartIndex, length: lineCharacterCount))
@ -1441,7 +1464,9 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
calculatedSegment.lines.append(InteractiveTextNodeLine( calculatedSegment.lines.append(InteractiveTextNodeLine(
line: line, line: line,
constrainedWidth: constrainedLineWidth,
frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)), frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)),
intrinsicWidth: lineWidth,
ascent: lineAscent, ascent: lineAscent,
descent: lineDescent, descent: lineDescent,
range: NSRange(location: currentLineStartIndex, length: lineCharacterCount), range: NSRange(location: currentLineStartIndex, length: lineCharacterCount),
@ -1453,6 +1478,11 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
attachments: [], attachments: [],
additionalTrailingLine: nil additionalTrailingLine: nil
)) ))
remainingLines -= 1
if remainingLines <= 0 {
break
}
} }
additionalSegmentRightInset = 0.0 additionalSegmentRightInset = 0.0
@ -1462,11 +1492,62 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
if currentLineStartIndex >= segmentEndIndex { if currentLineStartIndex >= segmentEndIndex {
break break
} }
if remainingLines <= 0 {
break
}
} }
calculatedSegments.append(calculatedSegment) calculatedSegments.append(calculatedSegment)
} }
if remainingLines <= 0, let lastSegment = calculatedSegments.last, let lastLine = lastSegment.lines.last, let lineRange = lastLine.range, let lineFont = attributedString.attribute(.font, at: lineRange.lowerBound, effectiveRange: nil) as? UIFont {
let truncatedTokenString: NSAttributedString
if let customTruncationTokenValue = customTruncationToken?(lineFont, lastSegment.blockQuote != nil) {
if lineRange.length == 0 && customTruncationTokenValue.string.hasPrefix("\u{2026} ") {
truncatedTokenString = customTruncationTokenValue.attributedSubstring(from: NSRange(location: 2, length: customTruncationTokenValue.length - 2))
} else {
truncatedTokenString = customTruncationTokenValue
}
} else {
var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:]
truncationTokenAttributes[NSAttributedString.Key.font] = lineFont
truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber
let tokenString = "\u{2026}"
truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes)
}
let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString)
var truncationTokenAscent: CGFloat = 0.0
var truncationTokenDescent: CGFloat = 0.0
let truncationTokenWidth = CTLineGetTypographicBounds(truncationToken, &truncationTokenAscent, &truncationTokenDescent, nil)
if let updatedLine = CTLineCreateTruncatedLine(lastLine.line, max(0.0, lastLine.constrainedWidth - truncationTokenWidth), .end, nil) {
var lineAscent: CGFloat = 0.0
var lineDescent: CGFloat = 0.0
var lineWidth = CTLineGetTypographicBounds(updatedLine, &lineAscent, &lineDescent, nil)
lineWidth = min(lineWidth, lastLine.constrainedWidth)
lastSegment.lines[lastSegment.lines.count - 1] = InteractiveTextNodeLine(
line: updatedLine,
constrainedWidth: lastLine.constrainedWidth,
frame: CGRect(origin: lastLine.frame.origin, size: CGSize(width: lineWidth, height: lineAscent + lineDescent)),
intrinsicWidth: lineWidth,
ascent: lineAscent,
descent: lineDescent,
range: lastLine.range,
isRTL: lastLine.isRTL,
strikethroughs: [],
spoilers: [],
spoilerWords: [],
embeddedItems: [],
attachments: [],
additionalTrailingLine: (truncationToken, 0.0)
)
}
}
var size = CGSize() var size = CGSize()
let isTruncated = false let isTruncated = false
@ -1642,8 +1723,8 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
} }
var segmentBlockQuote: InteractiveTextNodeBlockQuote? var segmentBlockQuote: InteractiveTextNodeBlockQuote?
if let blockQuote = segment.blockQuote, let tintColor = segment.tintColor, let blockIndex { if let blockQuote = segment.blockQuote, let tintColor = segment.tintColor, let blockIndex, let firstLine = segment.lines.first, let lastLine = segment.lines.last {
segmentBlockQuote = InteractiveTextNodeBlockQuote(id: blockIndex, frame: CGRect(origin: CGPoint(x: 0.0, y: blockMinY - 2.0), size: CGSize(width: blockWidth, height: blockMaxY - (blockMinY - 2.0) + 3.0)), data: blockQuote, tintColor: tintColor, secondaryTintColor: segment.secondaryTintColor, tertiaryTintColor: segment.tertiaryTintColor, backgroundColor: blockQuote.backgroundColor, isCollapsed: (blockQuote.isCollapsible && segmentLines.count > 3) ? isCollapsed : nil) segmentBlockQuote = InteractiveTextNodeBlockQuote(id: blockIndex, frame: CGRect(origin: CGPoint(x: 0.0, y: blockMinY + floor(0.15 * firstLine.frame.height)), size: CGSize(width: blockWidth, height: blockMaxY - blockMinY + floor(0.4 * lastLine.frame.height))), data: blockQuote, tintColor: tintColor, secondaryTintColor: segment.secondaryTintColor, tertiaryTintColor: segment.tertiaryTintColor, backgroundColor: blockQuote.backgroundColor, isCollapsed: (blockQuote.isCollapsible && segmentLines.count > 3) ? isCollapsed : nil)
} }
segments.append(InteractiveTextNodeSegment( segments.append(InteractiveTextNodeSegment(
@ -1692,7 +1773,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
) )
} }
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)?, displayContentsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?, expandedBlocks: Set<Int>) -> InteractiveTextNodeLayout { 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)?, displayContentsUnderSpoilers: Bool, customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)?, expandedBlocks: Set<Int>) -> InteractiveTextNodeLayout {
guard let attributedString else { guard let attributedString else {
return InteractiveTextNodeLayout(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, segments: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, expandedBlocks: expandedBlocks) return InteractiveTextNodeLayout(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, segments: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, expandedBlocks: expandedBlocks)
} }
@ -2171,7 +2252,7 @@ final class TextContentItemLayer: SimpleLayer {
} }
if let (additionalTrailingLine, _) = line.additionalTrailingLine { if let (additionalTrailingLine, _) = line.additionalTrailingLine {
context.textPosition = CGPoint(x: lineFrame.maxX, y: lineFrame.minY) context.textPosition = CGPoint(x: lineFrame.minX + line.intrinsicWidth, y: lineFrame.maxY - line.descent)
let glyphRuns = CTLineGetGlyphRuns(additionalTrailingLine) as NSArray let glyphRuns = CTLineGetGlyphRuns(additionalTrailingLine) as NSArray
if glyphRuns.count != 0 { if glyphRuns.count != 0 {
@ -2302,25 +2383,28 @@ final class TextContentItemLayer: SimpleLayer {
} }
blockExpandArrow.layerTintColor = blockQuote.tintColor.cgColor blockExpandArrow.layerTintColor = blockQuote.tintColor.cgColor
let blockBackgroundFrame = blockQuote.frame.offsetBy(dx: params.item.contentOffset.x, dy: params.item.contentOffset.y + 4.0) let blockBackgroundFrame = blockQuote.frame.offsetBy(dx: params.item.contentOffset.x, dy: params.item.contentOffset.y)
if animation.isAnimated { if animation.isAnimated {
self.isAnimating = true if blockBackgroundFrame != blockBackgroundView.layer.frame {
self.currentAnimationId += 1 self.isAnimating = true
let animationId = self.currentAnimationId self.currentAnimationId += 1
animation.animator.updateFrame(layer: blockBackgroundView.layer, frame: blockBackgroundFrame, completion: { [weak self] completed in let animationId = self.currentAnimationId
guard completed, let self, self.currentAnimationId == animationId, let params = self.params else {
return animation.animator.updateFrame(layer: blockBackgroundView.layer, frame: blockBackgroundFrame, completion: { [weak self] completed in
} guard completed, let self, self.currentAnimationId == animationId, let params = self.params else {
self.isAnimating = false return
self.update( }
params: params, self.isAnimating = false
animation: .None, self.update(
synchronously: true, params: params,
animateContents: false, animation: .None,
spoilerExpandRect: nil synchronously: true,
) animateContents: false,
}) spoilerExpandRect: nil
)
})
}
} else { } else {
blockBackgroundView.layer.frame = blockBackgroundFrame blockBackgroundView.layer.frame = blockBackgroundFrame
} }
@ -2431,9 +2515,9 @@ final class TextContentItemLayer: SimpleLayer {
} else { } else {
if let contentMaskNode = self.contentMaskNode { if let contentMaskNode = self.contentMaskNode {
self.contentMaskNode = nil self.contentMaskNode = nil
self.renderNode.layer.mask = nil
contentMaskNode.layer.removeFromSuperlayer() contentMaskNode.layer.removeFromSuperlayer()
} }
self.renderNode.layer.mask = nil
} }
if !params.item.segment.spoilers.isEmpty { if !params.item.segment.spoilers.isEmpty {
@ -2473,6 +2557,36 @@ final class TextContentItemLayer: SimpleLayer {
overlayContentLayer.frame = effectiveContentFrame overlayContentLayer.frame = effectiveContentFrame
} }
if let contentMask {
var overlayContentMaskAnimation = animation
let overlayContentMaskNode: ASImageNode
if let current = self.overlayContentMaskNode {
overlayContentMaskNode = current
} else {
overlayContentMaskNode = ASImageNode()
overlayContentMaskNode.isLayerBacked = true
overlayContentMaskNode.backgroundColor = .clear
self.overlayContentMaskNode = overlayContentMaskNode
overlayContentLayer.mask = overlayContentMaskNode.layer
if let currentContentMask = self.currentContentMask {
overlayContentMaskNode.frame = currentContentMask.frame
} else {
overlayContentMaskAnimation = .None
}
overlayContentMaskNode.image = contentMask.image
}
overlayContentMaskAnimation.animator.updateBackgroundColor(layer: overlayContentMaskNode.layer, color: contentMask.isOpaque ? UIColor.white : UIColor.clear, completion: nil)
overlayContentMaskAnimation.animator.updateFrame(layer: overlayContentMaskNode.layer, frame: contentMask.frame, completion: nil)
} else {
if let _ = self.overlayContentMaskNode {
self.overlayContentMaskNode = nil
overlayContentLayer.mask = nil
}
}
if let spoilerEffectNode = self.spoilerEffectNode { if let spoilerEffectNode = self.spoilerEffectNode {
if spoilerEffectNode.layer.superlayer !== overlayContentLayer { if spoilerEffectNode.layer.superlayer !== overlayContentLayer {
overlayContentLayer.addSublayer(spoilerEffectNode.layer) overlayContentLayer.addSublayer(spoilerEffectNode.layer)
@ -2593,6 +2707,7 @@ final class TextContentItemLayer: SimpleLayer {
if let spoilerEffectNode = self.spoilerEffectNode { if let spoilerEffectNode = self.spoilerEffectNode {
animation.transition.updateAlpha(layer: spoilerEffectNode.layer, alpha: params.item.displayContentsUnderSpoilers ? 0.0 : 1.0) animation.transition.updateAlpha(layer: spoilerEffectNode.layer, alpha: params.item.displayContentsUnderSpoilers ? 0.0 : 1.0)
spoilerEffectNode.update(revealed: params.item.displayContentsUnderSpoilers, animated: animation.isAnimated)
} }
} }
} }

View File

@ -97,6 +97,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent", "//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent",
"//submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen", "//submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen",
"//submodules/TelegramUI/Components/SliderContextItem", "//submodules/TelegramUI/Components/SliderContextItem",
"//submodules/TelegramUI/Components/InteractiveTextComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -8,13 +8,13 @@ import Postbox
import TelegramCore import TelegramCore
import TextNodeWithEntities import TextNodeWithEntities
import TextFormat import TextFormat
import InvisibleInkDustNode
import UrlEscaping import UrlEscaping
import TelegramPresentationData import TelegramPresentationData
import TextSelectionNode import TextSelectionNode
import SwiftSignalKit import SwiftSignalKit
import ForwardInfoPanelComponent import ForwardInfoPanelComponent
import PlainButtonComponent import PlainButtonComponent
import InteractiveTextComponent
final class StoryContentCaptionComponent: Component { final class StoryContentCaptionComponent: Component {
enum Action { enum Action {
@ -152,10 +152,8 @@ final class StoryContentCaptionComponent: Component {
} }
private final class ContentItem: UIView { private final class ContentItem: UIView {
var textNode: TextNodeWithEntities? var textNode: InteractiveTextNodeWithEntities?
var spoilerTextNode: TextNodeWithEntities?
var linkHighlightingNode: LinkHighlightingNode? var linkHighlightingNode: LinkHighlightingNode?
var dustNode: InvisibleInkDustNode?
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
@ -198,6 +196,8 @@ final class StoryContentCaptionComponent: Component {
private var ignoreScrolling: Bool = false private var ignoreScrolling: Bool = false
private var ignoreExternalState: Bool = false private var ignoreExternalState: Bool = false
private var displayContentsUnderSpoilers: (value: Bool, location: CGPoint?) = (false, nil)
private var expandedContentsBlocks: Set<Int> = Set()
private var isExpanded: Bool = false private var isExpanded: Bool = false
private var codeHighlight: CachedMessageSyntaxHighlight? private var codeHighlight: CachedMessageSyntaxHighlight?
@ -449,20 +449,17 @@ final class StoryContentCaptionComponent: Component {
} }
let contentItem = self.isExpanded ? self.expandedText : self.collapsedText let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
let otherContentItem = !self.isExpanded ? self.expandedText : self.collapsedText
switch recognizer.state { switch recognizer.state {
case .ended: case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let component = self.component, let textNode = contentItem.textNode { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let component = self.component, let textNode = contentItem.textNode {
let titleFrame = textNode.textNode.view.bounds let titleFrame = textNode.textNode.view.bounds
if titleFrame.contains(location) { if titleFrame.contains(location) {
if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { let textLocalPoint = CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)
if let (index, attributes) = textNode.textNode.attributesAtPoint(textLocalPoint) {
let action: Action? let action: Action?
if case .tap = gesture, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(contentItem.dustNode?.isRevealed ?? true) { if case .tap = gesture, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !self.displayContentsUnderSpoilers.value {
let convertedPoint = recognizer.view?.convert(location, to: contentItem.dustNode?.view) ?? location self.updateDisplayContentsUnderSpoilers(value: true, at: recognizer.view?.convert(location, to: textNode.textNode.view) ?? location)
contentItem.dustNode?.revealAtLocation(convertedPoint)
otherContentItem.dustNode?.revealAtLocation(convertedPoint)
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
return return
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true var concealed = true
@ -497,7 +494,17 @@ final class StoryContentCaptionComponent: Component {
if component.externalState.isSelectingText { if component.externalState.isSelectingText {
self.cancelTextSelection() self.cancelTextSelection()
} else if self.isExpanded { } else if self.isExpanded {
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) if let blockIndex = textNode.textNode.collapsibleBlockAtPoint(textLocalPoint) {
if self.expandedContentsBlocks.contains(blockIndex) {
self.expandedContentsBlocks.remove(blockIndex)
} else {
self.expandedContentsBlocks.insert(blockIndex)
}
self.state?.updated(transition: .spring(duration: 0.4))
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} else {
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
} else { } else {
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} }
@ -531,6 +538,14 @@ final class StoryContentCaptionComponent: Component {
} }
} }
private func updateDisplayContentsUnderSpoilers(value: Bool, at location: CGPoint?) {
if self.displayContentsUnderSpoilers.value == value {
return
}
self.displayContentsUnderSpoilers = (value, location)
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
private func updateTouchesAtPoint(_ point: CGPoint?) { private func updateTouchesAtPoint(_ point: CGPoint?) {
let contentItem = self.isExpanded ? self.expandedText : self.collapsedText let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
@ -563,7 +578,7 @@ final class StoryContentCaptionComponent: Component {
} }
} }
if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, let dustNode = contentItem.dustNode, !dustNode.isRevealed { if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, !self.displayContentsUnderSpoilers.value {
} else if let rects = rects { } else if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode let linkHighlightingNode: LinkHighlightingNode
if let current = contentItem.linkHighlightingNode { if let current = contentItem.linkHighlightingNode {
@ -652,46 +667,41 @@ final class StoryContentCaptionComponent: Component {
cachedMessageSyntaxHighlight: self.codeHighlight cachedMessageSyntaxHighlight: self.codeHighlight
) )
let truncationToken = NSMutableAttributedString() let truncationTokenString = component.strings.Story_CaptionShowMore
truncationToken.append(NSAttributedString(string: "\u{2026} ", font: Font.regular(16.0), textColor: .white)) let customTruncationToken: (UIFont, Bool) -> NSAttributedString? = { baseFont, _ in
truncationToken.append(NSAttributedString(string: component.strings.Story_CaptionShowMore, font: Font.semibold(16.0), textColor: .white)) let truncationToken = NSMutableAttributedString()
truncationToken.append(NSAttributedString(string: "\u{2026} ", font: Font.regular(baseFont.pointSize), textColor: .white))
truncationToken.append(NSAttributedString(string: truncationTokenString, font: Font.semibold(baseFont.pointSize), textColor: .white))
return truncationToken
}
let collapsedTextLayout = TextNodeWithEntities.asyncLayout(self.collapsedText.textNode)(TextNodeLayoutArguments( let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0)
let collapsedTextLayout = InteractiveTextNodeWithEntities.asyncLayout(self.collapsedText.textNode)(InteractiveTextNodeLayoutArguments(
attributedString: attributedText, attributedString: attributedText,
maximumNumberOfLines: 3, maximumNumberOfLines: 3,
truncationType: .end, truncationType: .end,
constrainedSize: CGSize(width: textContainerSize.width, height: 10000.0), constrainedSize: CGSize(width: textContainerSize.width, height: 10000.0),
insets: textInsets,
textShadowColor: UIColor(white: 0.0, alpha: 0.25), textShadowColor: UIColor(white: 0.0, alpha: 0.25),
textShadowBlur: 4.0, textShadowBlur: 4.0,
displaySpoilers: false, displayContentsUnderSpoilers: self.displayContentsUnderSpoilers.value,
customTruncationToken: truncationToken customTruncationToken: customTruncationToken,
expandedBlocks: self.expandedContentsBlocks
)) ))
let expandedTextLayout = TextNodeWithEntities.asyncLayout(self.expandedText.textNode)(TextNodeLayoutArguments( let expandedTextLayout = InteractiveTextNodeWithEntities.asyncLayout(self.expandedText.textNode)(InteractiveTextNodeLayoutArguments(
attributedString: attributedText, attributedString: attributedText,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
truncationType: .end, truncationType: .end,
constrainedSize: CGSize(width: textContainerSize.width, height: 10000.0), constrainedSize: CGSize(width: textContainerSize.width, height: 10000.0),
insets: textInsets,
textShadowColor: UIColor(white: 0.0, alpha: 0.25), textShadowColor: UIColor(white: 0.0, alpha: 0.25),
textShadowBlur: 4.0, textShadowBlur: 4.0,
displaySpoilers: false displayContentsUnderSpoilers: self.displayContentsUnderSpoilers.value,
expandedBlocks: self.expandedContentsBlocks
)) ))
let collapsedSpoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? let visibleTextHeight = collapsedTextLayout.0.size.height - textInsets.top - textInsets.bottom
if !collapsedTextLayout.0.spoilers.isEmpty { let textOverflowHeight: CGFloat = expandedTextLayout.0.size.height - textInsets.top - textInsets.bottom - visibleTextHeight
collapsedSpoilerTextLayoutAndApply = TextNodeWithEntities.asyncLayout(self.collapsedText.spoilerTextNode)(TextNodeLayoutArguments(attributedString: attributedText, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: textContainerSize.width, height: 10000.0), textShadowColor: UIColor(white: 0.0, alpha: 0.25), textShadowBlur: 4.0, displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true))
} else {
collapsedSpoilerTextLayoutAndApply = nil
}
let expandedSpoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
if !expandedTextLayout.0.spoilers.isEmpty {
expandedSpoilerTextLayoutAndApply = TextNodeWithEntities.asyncLayout(self.expandedText.spoilerTextNode)(TextNodeLayoutArguments(attributedString: attributedText, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: textContainerSize.width, height: 10000.0), textShadowColor: UIColor(white: 0.0, alpha: 0.25), textShadowBlur: 4.0, displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true))
} else {
expandedSpoilerTextLayoutAndApply = nil
}
let visibleTextHeight = collapsedTextLayout.0.size.height
let textOverflowHeight: CGFloat = expandedTextLayout.0.size.height - visibleTextHeight
let scrollContentSize = CGSize(width: availableSize.width, height: availableSize.height + textOverflowHeight) let scrollContentSize = CGSize(width: availableSize.width, height: availableSize.height + textOverflowHeight)
if let forwardInfo = component.forwardInfo { if let forwardInfo = component.forwardInfo {
@ -789,14 +799,55 @@ final class StoryContentCaptionComponent: Component {
forwardInfoPanel.view?.removeFromSuperview() forwardInfoPanel.view?.removeFromSuperview()
} }
let collapsedTextFrame = CGRect(origin: CGPoint(x: sideInset - textInsets.left, y: availableSize.height - visibleTextHeight - verticalInset - textInsets.top), size: collapsedTextLayout.0.size)
let expandedTextFrame = CGRect(origin: CGPoint(x: sideInset - textInsets.left, y: availableSize.height - visibleTextHeight - verticalInset - textInsets.top), size: expandedTextLayout.0.size)
var spoilerExpandRect: CGRect?
if let location = self.displayContentsUnderSpoilers.location {
self.displayContentsUnderSpoilers.location = nil
let mappedLocation = CGPoint(x: location.x, y: location.y)
let getDistance: (CGPoint, CGPoint) -> CGFloat = { a, b in
let v = CGPoint(x: a.x - b.x, y: a.y - b.y)
return sqrt(v.x * v.x + v.y * v.y)
}
var maxDistance: CGFloat = getDistance(mappedLocation, CGPoint(x: 0.0, y: 0.0))
maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: expandedTextFrame.width, y: 0.0)))
maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: expandedTextFrame.width, y: expandedTextFrame.height)))
maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: 0.0, y: expandedTextFrame.height)))
let mappedSize = CGSize(width: maxDistance * 2.0, height: maxDistance * 2.0)
spoilerExpandRect = mappedSize.centered(around: mappedLocation)
}
let textAnimation: ListViewItemUpdateAnimation
if case let .curve(duration, curve) = transition.animation {
textAnimation = .System(duration: duration, transition: ControlledTransition(duration: duration, curve: curve.containedViewLayoutTransitionCurve, interactive: false))
} else {
textAnimation = .None
}
let textApplyArguments = InteractiveTextNodeWithEntities.Arguments(
context: component.context,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 0.2, alpha: 1.0),
attemptSynchronous: true,
textColor: .white,
spoilerEffectColor: .white,
applyArguments: InteractiveTextNode.ApplyArguments(
animation: textAnimation,
spoilerTextColor: .white,
spoilerEffectColor: .white,
areContentAnimationsEnabled: true,
spoilerExpandRect: spoilerExpandRect
)
)
do { do {
let collapsedTextNode = collapsedTextLayout.1(TextNodeWithEntities.Arguments( let collapsedTextNode = collapsedTextLayout.1(textApplyArguments)
context: component.context,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 0.2, alpha: 1.0),
attemptSynchronous: true
))
if self.collapsedText.textNode !== collapsedTextNode { if self.collapsedText.textNode !== collapsedTextNode {
self.collapsedText.textNode?.textNode.view.removeFromSuperview() self.collapsedText.textNode?.textNode.view.removeFromSuperview()
@ -810,61 +861,11 @@ final class StoryContentCaptionComponent: Component {
collapsedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0)) collapsedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
} }
let collapsedTextFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: collapsedTextLayout.0.size)
collapsedTextNode.textNode.frame = collapsedTextFrame collapsedTextNode.textNode.frame = collapsedTextFrame
if let (_, collapsedSpoilerTextApply) = collapsedSpoilerTextLayoutAndApply {
let collapsedSpoilerTextNode = collapsedSpoilerTextApply(TextNodeWithEntities.Arguments(
context: component.context,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 0.2, alpha: 1.0),
attemptSynchronous: true
))
if self.collapsedText.spoilerTextNode == nil {
collapsedSpoilerTextNode.textNode.alpha = 0.0
collapsedSpoilerTextNode.textNode.isUserInteractionEnabled = false
collapsedSpoilerTextNode.textNode.contentMode = .topLeft
collapsedSpoilerTextNode.textNode.contentsScale = UIScreenScale
collapsedSpoilerTextNode.textNode.displaysAsynchronously = false
self.collapsedText.insertSubview(collapsedSpoilerTextNode.textNode.view, belowSubview: collapsedTextNode.textNode.view)
collapsedSpoilerTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
self.collapsedText.spoilerTextNode = collapsedSpoilerTextNode
}
self.collapsedText.spoilerTextNode?.textNode.frame = collapsedTextFrame
let collapsedDustNode: InvisibleInkDustNode
if let current = self.collapsedText.dustNode {
collapsedDustNode = current
} else {
collapsedDustNode = InvisibleInkDustNode(textNode: collapsedSpoilerTextNode.textNode, enableAnimations: component.context.sharedContext.energyUsageSettings.fullTranslucency)
self.collapsedText.dustNode = collapsedDustNode
self.collapsedText.insertSubview(collapsedDustNode.view, aboveSubview: collapsedSpoilerTextNode.textNode.view)
}
collapsedDustNode.frame = collapsedTextFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 0.0)
collapsedDustNode.update(size: collapsedDustNode.frame.size, color: .white, textColor: .white, rects: collapsedTextLayout.0.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: collapsedTextLayout.0.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
} else if let collapsedSpoilerTextNode = self.collapsedText.spoilerTextNode {
self.collapsedText.spoilerTextNode = nil
collapsedSpoilerTextNode.textNode.removeFromSupernode()
if let collapsedDustNode = self.collapsedText.dustNode {
self.collapsedText.dustNode = nil
collapsedDustNode.view.removeFromSuperview()
}
}
} }
do { do {
let expandedTextNode = expandedTextLayout.1(TextNodeWithEntities.Arguments( let expandedTextNode = expandedTextLayout.1(textApplyArguments)
context: component.context,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 0.2, alpha: 1.0),
attemptSynchronous: true
))
if self.expandedText.textNode !== expandedTextNode { if self.expandedText.textNode !== expandedTextNode {
self.expandedText.textNode?.textNode.view.removeFromSuperview() self.expandedText.textNode?.textNode.view.removeFromSuperview()
@ -876,51 +877,7 @@ final class StoryContentCaptionComponent: Component {
expandedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0)) expandedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
} }
let expandedTextFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: expandedTextLayout.0.size)
expandedTextNode.textNode.frame = expandedTextFrame expandedTextNode.textNode.frame = expandedTextFrame
if let (_, expandedSpoilerTextApply) = expandedSpoilerTextLayoutAndApply {
let expandedSpoilerTextNode = expandedSpoilerTextApply(TextNodeWithEntities.Arguments(
context: component.context,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 0.2, alpha: 1.0),
attemptSynchronous: true
))
if self.expandedText.spoilerTextNode == nil {
expandedSpoilerTextNode.textNode.alpha = 0.0
expandedSpoilerTextNode.textNode.isUserInteractionEnabled = false
expandedSpoilerTextNode.textNode.contentMode = .topLeft
expandedSpoilerTextNode.textNode.contentsScale = UIScreenScale
expandedSpoilerTextNode.textNode.displaysAsynchronously = false
self.expandedText.insertSubview(expandedSpoilerTextNode.textNode.view, belowSubview: expandedTextNode.textNode.view)
expandedSpoilerTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
self.expandedText.spoilerTextNode = expandedSpoilerTextNode
}
self.expandedText.spoilerTextNode?.textNode.frame = expandedTextFrame
let expandedDustNode: InvisibleInkDustNode
if let current = self.expandedText.dustNode {
expandedDustNode = current
} else {
expandedDustNode = InvisibleInkDustNode(textNode: expandedSpoilerTextNode.textNode, enableAnimations: component.context.sharedContext.energyUsageSettings.fullTranslucency)
self.expandedText.dustNode = expandedDustNode
self.expandedText.insertSubview(expandedDustNode.view, aboveSubview: expandedSpoilerTextNode.textNode.view)
}
expandedDustNode.frame = expandedTextFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 0.0)
expandedDustNode.update(size: expandedDustNode.frame.size, color: .white, textColor: .white, rects: expandedTextLayout.0.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: expandedTextLayout.0.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
} else if let expandedSpoilerTextNode = self.expandedText.spoilerTextNode {
self.expandedText.spoilerTextNode = nil
expandedSpoilerTextNode.textNode.removeFromSupernode()
if let expandedDustNode = self.expandedText.dustNode {
self.expandedText.dustNode = nil
expandedDustNode.view.removeFromSuperview()
}
}
} }
if self.textSelectionNode == nil, let controller = component.controller(), let textNode = self.expandedText.textNode?.textNode { if self.textSelectionNode == nil, let controller = component.controller(), let textNode = self.expandedText.textNode?.textNode {
@ -956,16 +913,6 @@ final class StoryContentCaptionComponent: Component {
} }
component.textSelectionAction(text, action) component.textSelectionAction(text, action)
}) })
/*textSelectionNode.updateRange = { [weak self] selectionRange in
if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange {
for (spoilerRange, _) in textLayout.spoilers {
if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 {
dustNode.update(revealed: true)
return
}
}
}
}*/
textSelectionNode.enableLookup = true textSelectionNode.enableLookup = true
self.textSelectionNode = textSelectionNode self.textSelectionNode = textSelectionNode
self.scrollView.addSubview(textSelectionNode.view) self.scrollView.addSubview(textSelectionNode.view)
@ -985,7 +932,7 @@ final class StoryContentCaptionComponent: Component {
if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
let action: Action? let action: Action?
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(contentItem.dustNode?.isRevealed ?? true) { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !self.displayContentsUnderSpoilers.value {
return false return false
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true var concealed = true
@ -1014,15 +961,9 @@ final class StoryContentCaptionComponent: Component {
return true return true
} }
//let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
//textSelectionNode.view.addGestureRecognizer(tapRecognizer)
let _ = textSelectionNode.view let _ = textSelectionNode.view
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
/*if let selectionRecognizer = textSelectionNode.recognizer {
recognizer.require(toFail: selectionRecognizer)
}*/
recognizer.tapActionAtPoint = { point in recognizer.tapActionAtPoint = { point in
return .waitForSingleTap return .waitForSingleTap
} }
@ -1050,6 +991,7 @@ final class StoryContentCaptionComponent: Component {
) )
self.ignoreScrolling = true self.ignoreScrolling = true
let previousBounds = self.scrollView.bounds
if self.scrollView.contentSize != scrollContentSize { if self.scrollView.contentSize != scrollContentSize {
self.scrollView.contentSize = scrollContentSize self.scrollView.contentSize = scrollContentSize
@ -1057,27 +999,13 @@ final class StoryContentCaptionComponent: Component {
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
transition.setFrame(view: self.scrollViewContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) transition.setFrame(view: self.scrollViewContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
/*if self.shadowGradientLayer.colors == nil { if !previousBounds.isEmpty, !transition.animation.isImmediate {
var locations: [NSNumber] = [] let bounds = self.scrollView.bounds
var colors: [CGColor] = [] if bounds.maxY != previousBounds.maxY {
let numStops = 10 let offsetY = previousBounds.maxY - bounds.maxY
let baseAlpha: CGFloat = 0.6 transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
for i in 0 ..< numStops {
let step = 1.0 - CGFloat(i) / CGFloat(numStops - 1)
locations.append((1.0 - step) as NSNumber)
let alphaStep: CGFloat = pow(step, 1.0)
colors.append(UIColor.black.withAlphaComponent(alphaStep * baseAlpha).cgColor)
} }
}
self.shadowGradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
self.shadowGradientLayer.endPoint = CGPoint(x: 0.0, y: 0.0)
self.shadowGradientLayer.locations = locations
self.shadowGradientLayer.colors = colors
self.shadowGradientLayer.type = .axial
self.shadowPlainLayer.backgroundColor = UIColor(white: 0.0, alpha: baseAlpha).cgColor
}*/
self.ignoreScrolling = false self.ignoreScrolling = false
self.updateScrolling(transition: transition) self.updateScrolling(transition: transition)
@ -1100,33 +1028,6 @@ final class StoryContentCaptionComponent: Component {
isExpandedTransition.setAlpha(view: self.collapsedText, alpha: self.isExpanded ? 0.0 : 1.0) isExpandedTransition.setAlpha(view: self.collapsedText, alpha: self.isExpanded ? 0.0 : 1.0)
isExpandedTransition.setAlpha(view: self.expandedText, alpha: !self.isExpanded ? 0.0 : 1.0) isExpandedTransition.setAlpha(view: self.expandedText, alpha: !self.isExpanded ? 0.0 : 1.0)
/*if let spoilerTextNode = self.collapsedText.spoilerTextNode {
var spoilerAlpha = self.isExpanded ? 0.0 : 1.0
if let dustNode = self.collapsedText.dustNode, dustNode.isRevealed {
} else {
spoilerAlpha = 0.0
}
isExpandedTransition.setAlpha(view: spoilerTextNode.textNode.view, alpha: spoilerAlpha)
}
if let dustNode = self.collapsedText.dustNode {
isExpandedTransition.setAlpha(view: dustNode.view, alpha: self.isExpanded ? 0.0 : 1.0)
}*/
/*if let textNode = self.expandedText.textNode {
isExpandedTransition.setAlpha(view: textNode.textNode.view, alpha: !self.isExpanded ? 0.0 : 1.0)
}
if let spoilerTextNode = self.expandedText.spoilerTextNode {
var spoilerAlpha = !self.isExpanded ? 0.0 : 1.0
if let dustNode = self.expandedText.dustNode, dustNode.isRevealed {
} else {
spoilerAlpha = 0.0
}
isExpandedTransition.setAlpha(view: spoilerTextNode.textNode.view, alpha: spoilerAlpha)
}
if let dustNode = self.expandedText.dustNode {
isExpandedTransition.setAlpha(view: dustNode.view, alpha: !self.isExpanded ? 0.0 : 1.0)
}*/
isExpandedTransition.setAlpha(view: self.shadowGradientView, alpha: self.isExpanded ? 0.0 : 1.0) isExpandedTransition.setAlpha(view: self.shadowGradientView, alpha: self.isExpanded ? 0.0 : 1.0)
isExpandedTransition.setAlpha(view: self.scrollBottomMaskView, alpha: self.isExpanded ? 1.0 : 0.0) isExpandedTransition.setAlpha(view: self.scrollBottomMaskView, alpha: self.isExpanded ? 1.0 : 0.0)

View File

@ -300,6 +300,59 @@ public final class TextFieldComponent: Component {
NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.font: Font.regular(17.0),
NSAttributedString.Key.foregroundColor: UIColor.white NSAttributedString.Key.foregroundColor: UIColor.white
] ]
self.textView.toggleQuoteCollapse = { [weak self] range in
guard let self else {
return
}
self.updateInputState { current in
let result = NSMutableAttributedString(attributedString: current.inputText)
var selectionRange = current.selectionRange
if let _ = result.attribute(ChatTextInputAttributes.block, at: range.lowerBound, effectiveRange: nil) as? ChatTextInputTextQuoteAttribute {
let blockString = NSMutableAttributedString(attributedString: result.attributedSubstring(from: range))
blockString.removeAttribute(ChatTextInputAttributes.block, range: NSRange(location: 0, length: blockString.length))
result.replaceCharacters(in: range, with: "")
result.insert(NSAttributedString(string: " ", attributes: [
ChatTextInputAttributes.collapsedBlock: blockString
]), at: range.lowerBound)
if selectionRange.lowerBound >= range.lowerBound && selectionRange.upperBound < range.upperBound {
selectionRange = range.lowerBound ..< range.lowerBound
} else if selectionRange.lowerBound >= range.upperBound {
let deltaLength = 1 - range.length
selectionRange = (selectionRange.lowerBound + deltaLength) ..< (selectionRange.lowerBound + deltaLength)
}
} else if let current = result.attribute(ChatTextInputAttributes.collapsedBlock, at: range.lowerBound, effectiveRange: nil) as? NSAttributedString {
result.replaceCharacters(in: range, with: "")
let updatedBlockString = NSMutableAttributedString(attributedString: current)
updatedBlockString.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false), range: NSRange(location: 0, length: updatedBlockString.length))
result.insert(updatedBlockString, at: range.lowerBound)
if selectionRange.lowerBound >= range.upperBound {
let deltaLength = updatedBlockString.length - 1
selectionRange = (selectionRange.lowerBound + deltaLength) ..< (selectionRange.lowerBound + deltaLength)
}
}
let stateResult = stateAttributedStringForText(result)
if selectionRange.lowerBound < 0 {
selectionRange = 0 ..< selectionRange.upperBound
}
if selectionRange.upperBound > stateResult.length {
selectionRange = selectionRange.lowerBound ..< stateResult.length
}
return InputState(
inputText: stateResult,
selectionRange: selectionRange
)
}
}
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -900,7 +953,7 @@ public final class TextFieldComponent: Component {
public func getAttributedText() -> NSAttributedString { public func getAttributedText() -> NSAttributedString {
Keyboard.applyAutocorrection(textView: self.textView) Keyboard.applyAutocorrection(textView: self.textView)
return self.inputState.inputText return expandedInputStateAttributedString(self.inputState.inputText)
} }
public func setAttributedText(_ string: NSAttributedString, updateState: Bool) { public func setAttributedText(_ string: NSAttributedString, updateState: Bool) {