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
}
var customTruncationToken: NSAttributedString?
var customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)?
var maximumNumberOfLines: Int = 0
if item.presentationData.isPreview {
if item.message.groupingKey != nil {
@ -573,10 +573,17 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
maximumNumberOfLines = 12
}
let truncationToken = NSMutableAttributedString()
truncationToken.append(NSAttributedString(string: "\u{2026} ", font: textFont, textColor: messageTheme.primaryTextColor))
truncationToken.append(NSAttributedString(string: item.presentationData.strings.Conversation_ReadMore, font: textFont, textColor: messageTheme.accentTextColor))
customTruncationToken = truncationToken
let truncationTokenText = item.presentationData.strings.Conversation_ReadMore
customTruncationToken = { baseFont, isQuote in
let truncationToken = NSMutableAttributedString()
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)
@ -1416,7 +1423,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return
}
self.displayContentsUnderSpoilers = (value, location)
//self.displayContentsUnderSpoilers.location = nil
if let item = self.item {
item.controllerInteraction.requestMessageUpdate(item.message.id, false)
}

View File

@ -35,12 +35,19 @@ private func generateBlockMaskImage() -> UIImage {
let colorSpace = CGColorSpaceCreateDeviceRGB()
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.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)
}
@ -96,7 +103,9 @@ private final class InteractiveTextNodeAttachment {
private final class InteractiveTextNodeLine {
let line: CTLine
let constrainedWidth: CGFloat
var frame: CGRect
let intrinsicWidth: CGFloat
let ascent: CGFloat
let descent: CGFloat
let range: NSRange?
@ -108,9 +117,11 @@ private final class InteractiveTextNodeLine {
var attachments: [InteractiveTextNodeAttachment]
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.constrainedWidth = constrainedWidth
self.frame = frame
self.intrinsicWidth = intrinsicWidth
self.ascent = ascent
self.descent = descent
self.range = range
@ -269,7 +280,7 @@ public final class InteractiveTextNodeLayoutArguments {
public let textShadowBlur: CGFloat?
public let textStroke: (UIColor, CGFloat)?
public let displayContentsUnderSpoilers: Bool
public let customTruncationToken: NSAttributedString?
public let customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)?
public let expandedBlocks: Set<Int>
public init(
@ -289,7 +300,7 @@ public final class InteractiveTextNodeLayoutArguments {
textShadowBlur: CGFloat? = nil,
textStroke: (UIColor, CGFloat)? = nil,
displayContentsUnderSpoilers: Bool = false,
customTruncationToken: NSAttributedString? = nil,
customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)? = nil,
expandedBlocks: Set<Int> = Set()
) {
self.attributedString = attributedString
@ -1256,7 +1267,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
textShadowBlur: CGFloat?,
textStroke: (UIColor, CGFloat)?,
displayContentsUnderSpoilers: Bool,
customTruncationToken: NSAttributedString?,
customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)?,
expandedBlocks: Set<Int>
) -> InteractiveTextNodeLayout {
let blockQuoteLeftInset: CGFloat = 9.0
@ -1348,7 +1359,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
}
}
struct CalculatedSegment {
class CalculatedSegment {
var titleLine: InteractiveTextNodeLine?
var lines: [InteractiveTextNodeLine] = []
var tintColor: UIColor?
@ -1356,16 +1367,18 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
var tertiaryTintColor: UIColor?
var blockQuote: TextNodeBlockQuoteData?
var additionalWidth: CGFloat = 0.0
init() {
}
}
var calculatedSegments: [CalculatedSegment] = []
var remainingLines = maximumNumberOfLines <= 0 ? Int.max : maximumNumberOfLines
for segment in stringSegments {
var calculatedSegment = CalculatedSegment()
calculatedSegment.blockQuote = segment.blockQuote
calculatedSegment.tintColor = segment.tintColor
calculatedSegment.secondaryTintColor = segment.secondaryTintColor
calculatedSegment.tertiaryTintColor = segment.tertiaryTintColor
if remainingLines <= 0 {
break
}
let rawSubstring = segment.substring.string as NSString
let substringLength = rawSubstring.length
@ -1376,6 +1389,12 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
var currentLineStartIndex = segment.firstCharacterOffset
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 additionalOffsetX: CGFloat = 0.0
if segment.blockQuote != nil {
@ -1398,13 +1417,16 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
if let title = segment.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 lineDescent: CGFloat = 0.0
let lineWidth = CTLineGetTypographicBounds(titleLine, &lineAscent, &lineDescent, nil)
calculatedSegment.titleLine = InteractiveTextNodeLine(
line: titleLine,
constrainedWidth: constrainedLineWidth,
frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)),
intrinsicWidth: lineWidth,
ascent: lineAscent,
descent: lineDescent,
range: nil,
@ -1421,7 +1443,8 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
}
while true {
let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, currentLineStartIndex, constrainedSegmentWidth - additionalSegmentRightInset)
let constrainedLineWidth = constrainedSegmentWidth - additionalSegmentRightInset
let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, currentLineStartIndex, constrainedLineWidth)
if lineCharacterCount != 0 {
let line = CTTypesetterCreateLine(typesetter, CFRange(location: currentLineStartIndex, length: lineCharacterCount))
@ -1441,7 +1464,9 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
calculatedSegment.lines.append(InteractiveTextNodeLine(
line: line,
constrainedWidth: constrainedLineWidth,
frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)),
intrinsicWidth: lineWidth,
ascent: lineAscent,
descent: lineDescent,
range: NSRange(location: currentLineStartIndex, length: lineCharacterCount),
@ -1453,6 +1478,11 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
attachments: [],
additionalTrailingLine: nil
))
remainingLines -= 1
if remainingLines <= 0 {
break
}
}
additionalSegmentRightInset = 0.0
@ -1462,11 +1492,62 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
if currentLineStartIndex >= segmentEndIndex {
break
}
if remainingLines <= 0 {
break
}
}
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()
let isTruncated = false
@ -1642,8 +1723,8 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
}
var segmentBlockQuote: InteractiveTextNodeBlockQuote?
if let blockQuote = segment.blockQuote, let tintColor = segment.tintColor, let blockIndex {
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)
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 + 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(
@ -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 {
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 {
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
if glyphRuns.count != 0 {
@ -2302,25 +2383,28 @@ final class TextContentItemLayer: SimpleLayer {
}
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 {
self.isAnimating = true
self.currentAnimationId += 1
let animationId = self.currentAnimationId
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 {
return
}
self.isAnimating = false
self.update(
params: params,
animation: .None,
synchronously: true,
animateContents: false,
spoilerExpandRect: nil
)
})
if blockBackgroundFrame != blockBackgroundView.layer.frame {
self.isAnimating = true
self.currentAnimationId += 1
let animationId = self.currentAnimationId
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 {
return
}
self.isAnimating = false
self.update(
params: params,
animation: .None,
synchronously: true,
animateContents: false,
spoilerExpandRect: nil
)
})
}
} else {
blockBackgroundView.layer.frame = blockBackgroundFrame
}
@ -2431,9 +2515,9 @@ final class TextContentItemLayer: SimpleLayer {
} else {
if let contentMaskNode = self.contentMaskNode {
self.contentMaskNode = nil
self.renderNode.layer.mask = nil
contentMaskNode.layer.removeFromSuperlayer()
}
self.renderNode.layer.mask = nil
}
if !params.item.segment.spoilers.isEmpty {
@ -2473,6 +2557,36 @@ final class TextContentItemLayer: SimpleLayer {
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 spoilerEffectNode.layer.superlayer !== overlayContentLayer {
overlayContentLayer.addSublayer(spoilerEffectNode.layer)
@ -2593,6 +2707,7 @@ final class TextContentItemLayer: SimpleLayer {
if let spoilerEffectNode = self.spoilerEffectNode {
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/StoryQualityUpgradeSheetScreen",
"//submodules/TelegramUI/Components/SliderContextItem",
"//submodules/TelegramUI/Components/InteractiveTextComponent",
],
visibility = [
"//visibility:public",

View File

@ -8,13 +8,13 @@ import Postbox
import TelegramCore
import TextNodeWithEntities
import TextFormat
import InvisibleInkDustNode
import UrlEscaping
import TelegramPresentationData
import TextSelectionNode
import SwiftSignalKit
import ForwardInfoPanelComponent
import PlainButtonComponent
import InteractiveTextComponent
final class StoryContentCaptionComponent: Component {
enum Action {
@ -152,10 +152,8 @@ final class StoryContentCaptionComponent: Component {
}
private final class ContentItem: UIView {
var textNode: TextNodeWithEntities?
var spoilerTextNode: TextNodeWithEntities?
var textNode: InteractiveTextNodeWithEntities?
var linkHighlightingNode: LinkHighlightingNode?
var dustNode: InvisibleInkDustNode?
override init(frame: CGRect) {
super.init(frame: frame)
@ -198,6 +196,8 @@ final class StoryContentCaptionComponent: Component {
private var ignoreScrolling: 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 codeHighlight: CachedMessageSyntaxHighlight?
@ -449,20 +449,17 @@ final class StoryContentCaptionComponent: Component {
}
let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
let otherContentItem = !self.isExpanded ? self.expandedText : self.collapsedText
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let component = self.component, let textNode = contentItem.textNode {
let titleFrame = textNode.textNode.view.bounds
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?
if case .tap = gesture, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(contentItem.dustNode?.isRevealed ?? true) {
let convertedPoint = recognizer.view?.convert(location, to: contentItem.dustNode?.view) ?? location
contentItem.dustNode?.revealAtLocation(convertedPoint)
otherContentItem.dustNode?.revealAtLocation(convertedPoint)
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
if case .tap = gesture, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !self.displayContentsUnderSpoilers.value {
self.updateDisplayContentsUnderSpoilers(value: true, at: recognizer.view?.convert(location, to: textNode.textNode.view) ?? location)
return
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true
@ -497,7 +494,17 @@ final class StoryContentCaptionComponent: Component {
if component.externalState.isSelectingText {
self.cancelTextSelection()
} 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 {
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?) {
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 {
let linkHighlightingNode: LinkHighlightingNode
if let current = contentItem.linkHighlightingNode {
@ -652,46 +667,41 @@ final class StoryContentCaptionComponent: Component {
cachedMessageSyntaxHighlight: self.codeHighlight
)
let truncationToken = NSMutableAttributedString()
truncationToken.append(NSAttributedString(string: "\u{2026} ", font: Font.regular(16.0), textColor: .white))
truncationToken.append(NSAttributedString(string: component.strings.Story_CaptionShowMore, font: Font.semibold(16.0), textColor: .white))
let truncationTokenString = component.strings.Story_CaptionShowMore
let customTruncationToken: (UIFont, Bool) -> NSAttributedString? = { baseFont, _ in
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,
maximumNumberOfLines: 3,
truncationType: .end,
constrainedSize: CGSize(width: textContainerSize.width, height: 10000.0),
insets: textInsets,
textShadowColor: UIColor(white: 0.0, alpha: 0.25),
textShadowBlur: 4.0,
displaySpoilers: false,
customTruncationToken: truncationToken
displayContentsUnderSpoilers: self.displayContentsUnderSpoilers.value,
customTruncationToken: customTruncationToken,
expandedBlocks: self.expandedContentsBlocks
))
let expandedTextLayout = TextNodeWithEntities.asyncLayout(self.expandedText.textNode)(TextNodeLayoutArguments(
let expandedTextLayout = InteractiveTextNodeWithEntities.asyncLayout(self.expandedText.textNode)(InteractiveTextNodeLayoutArguments(
attributedString: attributedText,
maximumNumberOfLines: 0,
truncationType: .end,
constrainedSize: CGSize(width: textContainerSize.width, height: 10000.0),
insets: textInsets,
textShadowColor: UIColor(white: 0.0, alpha: 0.25),
textShadowBlur: 4.0,
displaySpoilers: false
displayContentsUnderSpoilers: self.displayContentsUnderSpoilers.value,
expandedBlocks: self.expandedContentsBlocks
))
let collapsedSpoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
if !collapsedTextLayout.0.spoilers.isEmpty {
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 visibleTextHeight = collapsedTextLayout.0.size.height - textInsets.top - textInsets.bottom
let textOverflowHeight: CGFloat = expandedTextLayout.0.size.height - textInsets.top - textInsets.bottom - visibleTextHeight
let scrollContentSize = CGSize(width: availableSize.width, height: availableSize.height + textOverflowHeight)
if let forwardInfo = component.forwardInfo {
@ -789,14 +799,55 @@ final class StoryContentCaptionComponent: Component {
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 {
let collapsedTextNode = collapsedTextLayout.1(TextNodeWithEntities.Arguments(
context: component.context,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 0.2, alpha: 1.0),
attemptSynchronous: true
))
let collapsedTextNode = collapsedTextLayout.1(textApplyArguments)
if self.collapsedText.textNode !== collapsedTextNode {
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))
}
let collapsedTextFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: collapsedTextLayout.0.size)
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 {
let expandedTextNode = expandedTextLayout.1(TextNodeWithEntities.Arguments(
context: component.context,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 0.2, alpha: 1.0),
attemptSynchronous: true
))
let expandedTextNode = expandedTextLayout.1(textApplyArguments)
if self.expandedText.textNode !== expandedTextNode {
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))
}
let expandedTextFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: expandedTextLayout.0.size)
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 {
@ -956,16 +913,6 @@ final class StoryContentCaptionComponent: Component {
}
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
self.textSelectionNode = textSelectionNode
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)) {
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
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true
@ -1014,15 +961,9 @@ final class StoryContentCaptionComponent: Component {
return true
}
//let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
//textSelectionNode.view.addGestureRecognizer(tapRecognizer)
let _ = textSelectionNode.view
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
/*if let selectionRecognizer = textSelectionNode.recognizer {
recognizer.require(toFail: selectionRecognizer)
}*/
recognizer.tapActionAtPoint = { point in
return .waitForSingleTap
}
@ -1050,6 +991,7 @@ final class StoryContentCaptionComponent: Component {
)
self.ignoreScrolling = true
let previousBounds = self.scrollView.bounds
if 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.scrollViewContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
/*if self.shadowGradientLayer.colors == nil {
var locations: [NSNumber] = []
var colors: [CGColor] = []
let numStops = 10
let baseAlpha: CGFloat = 0.6
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)
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds
if bounds.maxY != previousBounds.maxY {
let offsetY = previousBounds.maxY - bounds.maxY
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
}
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.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.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.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.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) {
@ -900,7 +953,7 @@ public final class TextFieldComponent: Component {
public func getAttributedText() -> NSAttributedString {
Keyboard.applyAutocorrection(textView: self.textView)
return self.inputState.inputText
return expandedInputStateAttributedString(self.inputState.inputText)
}
public func setAttributedText(_ string: NSAttributedString, updateState: Bool) {