From 581024c5f025b5e23a80b9cdb3a4285eae926638 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 23 Dec 2021 18:35:46 +0400 Subject: [PATCH] Various Improvements --- .../Sources/InvisibleInkDustNode.swift | 12 +- .../TelegramUI/Sources/ChatController.swift | 1 + .../PeerSelectionTextInputPanelNode.swift | 188 +++++++++++++++++- 3 files changed, 186 insertions(+), 15 deletions(-) diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index 4eea9d8869..10c2a34041 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -99,9 +99,9 @@ public class InvisibleInkDustNode: ASDisplayNode { emitter.velocityRange = 20.0 emitter.name = "dustCell" emitter.alphaRange = 1.0 -// emitter.setValue("point", forKey: "particleType") -// emitter.setValue(3.0, forKey: "mass") -// emitter.setValue(2.0, forKey: "massRange") + emitter.setValue("point", forKey: "particleType") + emitter.setValue(3.0, forKey: "mass") + emitter.setValue(2.0, forKey: "massRange") self.emitter = emitter let fingerAttractor = createEmitterBehavior(type: "simpleAttractor") @@ -182,7 +182,7 @@ public class InvisibleInkDustNode: ASDisplayNode { var scaleAddition = maxFactor * 4.0 var durationAddition = -maxFactor * 0.2 if self.emitterNode.frame.height > 0.0, self.emitterNode.frame.width / self.emitterNode.frame.height < 0.3 { - scaleAddition *= 4.0 + scaleAddition *= 5.0 durationAddition *= 2.0 } @@ -292,7 +292,9 @@ public class InvisibleInkDustNode: ASDisplayNode { square += Float(rect.width * rect.height) } - self.emitter?.birthRate = min(120000, square * 0.35) + Queue.mainQueue().async { + self.emitter?.birthRate = min(100000, square * 0.35) + } } public func update(size: CGSize, color: UIColor, rects: [CGRect], wordRects: [CGRect]) { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 2405ede222..48ee69e457 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -9868,6 +9868,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) let inputPanelNode = PeerSelectionTextInputPanelNode(presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { _ in }) + inputPanelNode.context = self.context inputPanelNode.interfaceInteraction = interfaceInteraction inputPanelNode.effectivePresentationInterfaceState = { return presentationInterfaceState diff --git a/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift b/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift index 775e22681c..6e2e23917d 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift @@ -14,6 +14,7 @@ import ActivityIndicator import Speak import ObjCRuntimeUtils import LegacyComponents +import InvisibleInkDustNode private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) private let minInputFontSize = chatTextInputMinFontSize @@ -120,6 +121,7 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A let textInputContainerBackgroundNode: ASImageNode let textInputContainer: ASDisplayNode var textInputNode: CaptionEditableTextNode? + var dustNode: InvisibleInkDustNode? private var oneLineNode: ImmediateTextNode let textInputBackgroundNode: ASDisplayNode @@ -207,10 +209,11 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false) + textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) self.updatingInputState = false self.updateTextNodeText(animated: animated) + self.updateSpoiler() } } @@ -237,6 +240,8 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A private let textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0) + private var spoilersRevealed = false + init(presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, presentController: @escaping (ViewController) -> Void) { self.presentationInterfaceState = presentationInterfaceState self.isCaption = isCaption @@ -387,6 +392,8 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A let textInputFrame = self.textInputContainer.frame textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) + textInputNode.view.layoutIfNeeded() + self.updateSpoiler() } self.textInputBackgroundNode.isUserInteractionEnabled = false @@ -501,6 +508,8 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A textInputNode.selectedRange = selectedRange } textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] + + self.updateSpoiler() } } @@ -703,9 +712,11 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: false) + refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed) refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + self.updateSpoiler() + let inputTextState = self.inputTextState self.skipUpdate = true @@ -726,6 +737,148 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A } } + private func updateSpoiler() { + guard let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { + return + } + + let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + + var rects: [CGRect] = [] + + if let attributedText = textInputNode.attributedText { + let beginning = textInputNode.textView.beginningOfDocument + attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in + if let _ = attributes[ChatTextInputAttributes.spoiler] { + func addSpoiler(startIndex: Int, endIndex: Int) { + if let start = textInputNode.textView.position(from: beginning, offset: startIndex), let end = textInputNode.textView.position(from: start, offset: endIndex - startIndex), let textRange = textInputNode.textView.textRange(from: start, to: end) { + let textRects = textInputNode.textView.selectionRects(for: textRange) + for textRect in textRects { + rects.append(textRect.rect.insetBy(dx: 1.0, dy: 1.0).offsetBy(dx: 0.0, dy: 1.0)) + } + } + } + + var startIndex: Int? + var currentIndex: Int? + + let nsString = (attributedText.string as NSString) + nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in + if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { + if let currentStartIndex = startIndex { + startIndex = nil + let endIndex = range.location + addSpoiler(startIndex: currentStartIndex, endIndex: endIndex) + } + } else if startIndex == nil { + startIndex = range.location + } + currentIndex = range.location + range.length + } + + if let currentStartIndex = startIndex, let currentIndex = currentIndex { + startIndex = nil + let endIndex = currentIndex + addSpoiler(startIndex: currentStartIndex, endIndex: endIndex) + } + } + }) + } + + if !rects.isEmpty { + let dustNode: InvisibleInkDustNode + if let current = self.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.alpha = self.spoilersRevealed ? 0.0 : 1.0 + dustNode.isUserInteractionEnabled = false + textInputNode.textView.addSubview(dustNode.view) + self.dustNode = dustNode + } + dustNode.frame = CGRect(origin: CGPoint(), size: textInputNode.textView.contentSize) + dustNode.update(size: textInputNode.textView.contentSize, color: textColor, rects: rects, wordRects: rects) + } else if let dustNode = self.dustNode { + dustNode.removeFromSupernode() + self.dustNode = nil + } + } + + private func updateSpoilersRevealed(animated: Bool = true) { + guard let textInputNode = self.textInputNode else { + return + } + + let selectionRange = textInputNode.textView.selectedRange + + var revealed = false + if let attributedText = textInputNode.attributedText { + attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in + if let _ = attributes[ChatTextInputAttributes.spoiler] { + if let _ = selectionRange.intersection(range) { + revealed = true + } + } + }) + } + + guard self.spoilersRevealed != revealed else { + return + } + self.spoilersRevealed = revealed + + if revealed { + self.updateInternalSpoilersRevealed(true, animated: animated) + } else { + Queue.mainQueue().after(1.5, { + self.updateInternalSpoilersRevealed(false, animated: true) + }) + } + } + + private func updateInternalSpoilersRevealed(_ revealed: Bool, animated: Bool) { + guard self.spoilersRevealed == revealed, let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { + return + } + + let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + + refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed) + + textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) + + if textInputNode.textView.subviews.count > 1, animated { + let containerView = textInputNode.textView.subviews[1] + if let canvasView = containerView.subviews.first { + if let snapshotView = canvasView.snapshotView(afterScreenUpdates: false) { + textInputNode.view.insertSubview(snapshotView, at: 0) + canvasView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + } + + if animated { + if revealed { + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + if let dustNode = self.dustNode { + transition.updateAlpha(node: dustNode, alpha: 0.0) + } + } else { + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + if let dustNode = self.dustNode { + transition.updateAlpha(node: dustNode, alpha: 1.0) + } + } + } else if let dustNode = self.dustNode { + dustNode.alpha = revealed ? 0.0 : 1.0 + } + } + private func updateCounterTextNode(transition: ContainedViewLayoutTransition) { let inputTextMaxLength: Int32? if self.isCaption { @@ -830,7 +983,9 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A @objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { if !dueToEditing && !self.updatingInputState { let inputTextState = self.inputTextState + self.skipUpdate = true self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) + self.skipUpdate = false } if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { @@ -840,6 +995,8 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + + self.updateSpoilersRevealed() } } @@ -977,9 +1134,21 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A @objc func formatAttributesSpoiler(_ sender: Any) { self.inputMenu.back() + + var animated = false + if let attributedText = self.textInputNode?.attributedText { + attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in + if let _ = attributes[ChatTextInputAttributes.spoiler] { + animated = true + } + }) + } + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.spoiler), inputMode) } + + self.updateSpoilersRevealed(animated: animated) } @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { @@ -1005,7 +1174,7 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false) + let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) string.replaceCharacters(in: range, with: cleanReplacementString) self.textInputNode?.attributedText = string self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0) @@ -1049,12 +1218,6 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A } @objc func sendButtonPressed() { - if let sendPressed = self.sendPressed, let presentationInterfaceState = self.effectivePresentationInterfaceState?() { - self.dismissInput() - let effectiveInputText = presentationInterfaceState.interfaceState.composeInputState.inputText - sendPressed(effectiveInputText) - return - } let inputTextMaxLength: Int32? if self.isCaption { inputTextMaxLength = self.context?.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength @@ -1071,7 +1234,12 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A return } } - + if let sendPressed = self.sendPressed, let presentationInterfaceState = self.effectivePresentationInterfaceState?() { + self.dismissInput() + let effectiveInputText = presentationInterfaceState.interfaceState.composeInputState.inputText + sendPressed(effectiveInputText) + return + } self.sendMessage(.generic) }