diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index b987d51f8e..524619a59f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -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) } diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index cf280a74d0..b27589467e 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -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 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 = 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 ) -> 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) -> 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) -> 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) } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 6bb5c6bbe3..7e63c31eea 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 5c8132efaa..4684dde0d0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -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 = 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) diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index b9831240c9..0d31238442 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -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) {