From 16c66c7bad59a91f5cc41e39be0369902af38a02 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 4 Jul 2023 14:24:15 +0200 Subject: [PATCH] Story caption and reply text length limit --- .../State/UserLimitsConfiguration.swift | 9 +- .../Data/ConfigurationData.swift | 8 +- .../Sources/MediaEditorScreen.swift | 29 +++-- .../Sources/StoryPreviewComponent.swift | 1 + .../Sources/MessageInputPanelComponent.swift | 79 +++++++++++- .../Stories/StoryContainerScreen/BUILD | 1 + .../StoryContentCaptionComponent.swift | 78 ++++++------ .../StoryItemSetContainerComponent.swift | 46 +++++-- ...StoryItemSetContainerViewSendMessage.swift | 115 +++++++++++++++--- .../Sources/TextFieldComponent.swift | 6 + 10 files changed, 291 insertions(+), 81 deletions(-) diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index 100ed57144..6ec79782e3 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -17,6 +17,7 @@ public struct UserLimitsConfiguration: Equatable { public let maxReactionsPerMessage: Int32 public let maxSharedFolderInviteLinks: Int32 public let maxSharedFolderJoin: Int32 + public let maxStoryCaptionLength: Int32 public static var defaultValue: UserLimitsConfiguration { return UserLimitsConfiguration( @@ -34,7 +35,8 @@ public struct UserLimitsConfiguration: Equatable { maxAnimatedEmojisInText: 10, maxReactionsPerMessage: 1, maxSharedFolderInviteLinks: 3, - maxSharedFolderJoin: 2 + maxSharedFolderJoin: 2, + maxStoryCaptionLength: 1024 ) } @@ -53,7 +55,8 @@ public struct UserLimitsConfiguration: Equatable { maxAnimatedEmojisInText: Int32, maxReactionsPerMessage: Int32, maxSharedFolderInviteLinks: Int32, - maxSharedFolderJoin: Int32 + maxSharedFolderJoin: Int32, + maxStoryCaptionLength: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount @@ -70,6 +73,7 @@ public struct UserLimitsConfiguration: Equatable { self.maxReactionsPerMessage = maxReactionsPerMessage self.maxSharedFolderInviteLinks = maxSharedFolderInviteLinks self.maxSharedFolderJoin = maxSharedFolderJoin + self.maxStoryCaptionLength = maxStoryCaptionLength } } @@ -109,5 +113,6 @@ extension UserLimitsConfiguration { self.maxReactionsPerMessage = getValue("reactions_user_max", orElse: 1) self.maxSharedFolderInviteLinks = getValue("chatlist_invites_limit", orElse: isPremium ? 100 : 3) self.maxSharedFolderJoin = getValue("chatlists_joined_limit", orElse: isPremium ? 100 : 2) + self.maxStoryCaptionLength = getGeneralValue("story_caption_length_limit", orElse: defaultValue.maxStoryCaptionLength) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index 76897a712b..0ab6031eec 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -51,6 +51,7 @@ public enum EngineConfiguration { public let maxReactionsPerMessage: Int32 public let maxSharedFolderInviteLinks: Int32 public let maxSharedFolderJoin: Int32 + public let maxStoryCaptionLength: Int32 public static var defaultValue: UserLimits { return UserLimits(UserLimitsConfiguration.defaultValue) @@ -71,7 +72,8 @@ public enum EngineConfiguration { maxAnimatedEmojisInText: Int32, maxReactionsPerMessage: Int32, maxSharedFolderInviteLinks: Int32, - maxSharedFolderJoin: Int32 + maxSharedFolderJoin: Int32, + maxStoryCaptionLength: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount @@ -88,6 +90,7 @@ public enum EngineConfiguration { self.maxReactionsPerMessage = maxReactionsPerMessage self.maxSharedFolderInviteLinks = maxSharedFolderInviteLinks self.maxSharedFolderJoin = maxSharedFolderJoin + self.maxStoryCaptionLength = maxStoryCaptionLength } } } @@ -139,7 +142,8 @@ public extension EngineConfiguration.UserLimits { maxAnimatedEmojisInText: userLimitsConfiguration.maxAnimatedEmojisInText, maxReactionsPerMessage: userLimitsConfiguration.maxReactionsPerMessage, maxSharedFolderInviteLinks: userLimitsConfiguration.maxSharedFolderInviteLinks, - maxSharedFolderJoin: userLimitsConfiguration.maxSharedFolderJoin + maxSharedFolderJoin: userLimitsConfiguration.maxSharedFolderJoin, + maxStoryCaptionLength: userLimitsConfiguration.maxStoryCaptionLength ) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 34785fefb4..2d9eef6762 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -392,11 +392,20 @@ final class MediaEditorScreenComponent: Component { } @objc private func deactivateInput() { - self.currentInputMode = .text - if hasFirstResponder(self) { - self.endEditing(true) + guard let view = self.inputPanel.view as? MessageInputPanelComponent.View else { + return + } + if view.canDeactivateInput() { + self.currentInputMode = .text + if hasFirstResponder(self) { + if let view = self.inputPanel.view as? MessageInputPanelComponent.View { + view.deactivateInput() + } + } else { + self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged))) + } } else { - self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged))) + view.animateError() } } @@ -967,13 +976,14 @@ final class MediaEditorScreenComponent: Component { inputPanelAvailableWidth += 200.0 } } - if environment.inputHeight > 0.0 || self.currentInputMode == .emoji { + + let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden + if environment.inputHeight > 0.0 || self.currentInputMode == .emoji || keyboardWasHidden { inputPanelAvailableHeight = 200.0 } var inputHeight = environment.inputHeight var keyboardHeight = environment.deviceMetrics.standardInputHeight(inLandscape: false) - let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { let inputMediaNode: ChatEntityKeyboardInputNode @@ -1078,6 +1088,7 @@ final class MediaEditorScreenComponent: Component { strings: environment.strings, style: .editor, placeholder: "Add a caption...", + maxLength: Int(component.context.userLimits.maxStoryCaptionLength), queryTypes: [.mention], alwaysDarkWhenHasText: false, nextInputMode: { _ in return nextInputMode }, @@ -1424,10 +1435,8 @@ final class MediaEditorScreenComponent: Component { muteButtonView.layer.shadowOpacity = 0.35 self.addSubview(muteButtonView) - if self.animatingButtons { - muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: 0.1) - muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.1) - } + muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) + muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) } transition.setPosition(view: muteButtonView, position: muteButtonFrame.center) transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size)) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index 3d844f84a7..cf2cf00534 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -250,6 +250,7 @@ final class StoryPreviewComponent: Component { strings: presentationData.strings, style: .story, placeholder: "Reply Privately...", + maxLength: nil, queryTypes: [], alwaysDarkWhenHasText: false, nextInputMode: { _ in return .stickers }, diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index af07a5cc74..00e8414778 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -64,6 +64,7 @@ public final class MessageInputPanelComponent: Component { public let strings: PresentationStrings public let style: Style public let placeholder: String + public let maxLength: Int? public let queryTypes: ContextQueryTypes public let alwaysDarkWhenHasText: Bool public let nextInputMode: (Bool) -> InputMode? @@ -104,6 +105,7 @@ public final class MessageInputPanelComponent: Component { strings: PresentationStrings, style: Style, placeholder: String, + maxLength: Int?, queryTypes: ContextQueryTypes, alwaysDarkWhenHasText: Bool, nextInputMode: @escaping (Bool) -> InputMode?, @@ -144,6 +146,7 @@ public final class MessageInputPanelComponent: Component { self.style = style self.nextInputMode = nextInputMode self.placeholder = placeholder + self.maxLength = maxLength self.queryTypes = queryTypes self.alwaysDarkWhenHasText = alwaysDarkWhenHasText self.areVoiceMessagesAvailable = areVoiceMessagesAvailable @@ -196,6 +199,9 @@ public final class MessageInputPanelComponent: Component { if lhs.placeholder != rhs.placeholder { return false } + if lhs.maxLength != rhs.maxLength { + return false + } if lhs.queryTypes != rhs.queryTypes { return false } @@ -266,6 +272,8 @@ public final class MessageInputPanelComponent: Component { private let placeholder = ComponentView() private let vibrancyPlaceholder = ComponentView() + private let counter = ComponentView() + private var disabledPlaceholder: ComponentView? private let textField = ComponentView() private let textFieldExternalState = TextFieldComponent.ExternalState() @@ -297,6 +305,8 @@ public final class MessageInputPanelComponent: Component { private var viewForOverlayContent: ViewForOverlayContent? private var currentEmojiSuggestionView: ComponentHostView? + private let hapticFeedback = HapticFeedback() + private var component: MessageInputPanelComponent? private weak var state: EmptyComponentState? @@ -376,6 +386,30 @@ public final class MessageInputPanelComponent: Component { } } + public func canDeactivateInput() -> Bool { + guard let component = self.component else { + return true + } + if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength { + return false + } else { + return true + } + } + + public func deactivateInput() { + if self.canDeactivateInput() { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.deactivateInput() + } + } + } + + public func animateError() { + self.textField.view?.layer.addShakeAnimation() + self.hapticFeedback.error() + } + public func updateContextQueries() { guard let component = self.component, let textFieldView = self.textField.view as? TextFieldComponent.View else { return @@ -534,7 +568,6 @@ public final class MessageInputPanelComponent: Component { ) let isEditing = self.textFieldExternalState.isEditing || component.forceIsEditing - let placeholderSize = self.placeholder.update( transition: .immediate, component: AnyComponent(Text( @@ -568,7 +601,7 @@ public final class MessageInputPanelComponent: Component { } else { fieldBackgroundFrame = fieldFrame } - + transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size)) transition.setFrame(view: self.fieldBackgroundView, frame: fieldBackgroundFrame) @@ -659,6 +692,36 @@ public final class MessageInputPanelComponent: Component { } } + if let maxLength = component.maxLength, maxLength - self.textFieldExternalState.textLength < 5 { + let remainingLength = max(-999, maxLength - self.textFieldExternalState.textLength) + let counterSize = self.counter.update( + transition: .immediate, + component: AnyComponent(Text( + text: "\(remainingLength)", + font: Font.with(size: 14.0, traits: .monospacedNumbers), + color: self.textFieldExternalState.textLength > maxLength ? UIColor(rgb: 0xff3b30) : UIColor(rgb: 0xffffff, alpha: 0.25) + )), + environment: {}, + containerSize: availableTextFieldSize + ) + let counterFrame = CGRect(origin: CGPoint(x: availableSize.width - insets.right + floorToScreenPixels((insets.right - counterSize.width) * 0.5), y: size.height - insets.bottom - baseFieldHeight - counterSize.height - 5.0), size: counterSize) + if let counterView = self.counter.view { + if counterView.superview == nil { + self.addSubview(counterView) + counterView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + counterView.center = counterFrame.center + } else { + transition.setPosition(view: counterView, position: counterFrame.center) + } + counterView.bounds = CGRect(origin: .zero, size: counterFrame.size) + } + } else if let counterView = self.counter.view, counterView.superview != nil { + counterView.layer.animateAlpha(from: 1.00, to: 0.0, duration: 0.2, completion: { _ in + counterView.removeFromSuperview() + }) + } + if component.attachmentAction != nil { let attachmentButtonMode: MessageInputActionButtonComponent.Mode attachmentButtonMode = .attach @@ -830,12 +893,20 @@ public final class MessageInputPanelComponent: Component { component.sendMessageAction() } else if case let .text(string) = self.getSendMessageInput(), string.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { } else { - component.sendMessageAction() + if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength { + self.animateError() + } else { + component.sendMessageAction() + } } } case .apply: if case .up = action { - component.sendMessageAction() + if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength { + self.animateError() + } else { + component.sendMessageAction() + } } case .voiceInput, .videoInput: component.setMediaRecordingActive?(action == .down, mode == .videoInput, sendAction) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index e45a0c84e7..3ecdf7fef7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -73,6 +73,7 @@ swift_library( "//submodules/ImageBlur", "//submodules/StickerPackPreviewUI", "//submodules/Components/AnimatedStickerComponent", + "//submodules/OpenInExternalAppUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 8f367f54b6..ce43120c3d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -44,19 +44,22 @@ final class StoryContentCaptionComponent: Component { let text: String let entities: [MessageTextEntity] let action: (Action) -> Void + let longTapAction: (Action) -> Void init( externalState: ExternalState, context: AccountContext, text: String, entities: [MessageTextEntity], - action: @escaping (Action) -> Void + action: @escaping (Action) -> Void, + longTapAction: @escaping (Action) -> Void ) { self.externalState = externalState self.context = context self.text = text self.entities = entities self.action = action + self.longTapAction = longTapAction } static func ==(lhs: StoryContentCaptionComponent, rhs: StoryContentCaptionComponent) -> Bool { @@ -242,44 +245,47 @@ final class StoryContentCaptionComponent: Component { switch recognizer.state { case .ended: if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let component = self.component, let textNode = self.textNode { - switch gesture { - case .tap: - 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)) { - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) { - let convertedPoint = recognizer.view?.convert(location, to: self.dustNode?.view) ?? location - self.dustNode?.revealAtLocation(convertedPoint) - return - } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - var concealed = true - if let (attributeText, fullText) = textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { - concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) - } - component.action(.url(url: url, concealed: concealed)) - return - } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - component.action(.peerMention(peerId: peerMention.peerId, mention: peerMention.mention)) - return - } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - component.action(.textMention(peerName)) - return - } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - component.action(.hashtag(hashtag.peerName, hashtag.hashtag)) - return - } else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String { - component.action(.bankCard(bankCard)) - return - } else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file { - component.action(.customEmoji(file)) - return + 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 action: Action? + if case .tap = gesture, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) { + let convertedPoint = recognizer.view?.convert(location, to: self.dustNode?.view) ?? location + self.dustNode?.revealAtLocation(convertedPoint) + return + } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + var concealed = true + if let (attributeText, fullText) = textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } + action = .url(url: url, concealed: concealed) + } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { + action = .peerMention(peerId: peerMention.peerId, mention: peerMention.mention) + } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + action = .textMention(peerName) + } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { + action = .hashtag(hashtag.peerName, hashtag.hashtag) + } else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String { + action = .bankCard(bankCard) + } else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file { + action = .customEmoji(file) + } else { + action = nil } + guard let action else { + return + } + switch gesture { + case .tap: + component.action(action) + case .longTap: + component.longTapAction(action) + default: + return + } + self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + return } - - self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) - default: - break } } default: diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 2fbf36839c..f2543f275a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -645,14 +645,25 @@ public final class StoryItemSetContainerComponent: Component { return false } + private func deactivateInput() { + + } + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state, let component = self.component, let itemLayout = self.itemLayout { - if hasFirstResponder(self) { - self.sendMessageContext.currentInputMode = .text - self.endEditing(true) - } else if case .media = self.sendMessageContext.currentInputMode { - self.sendMessageContext.currentInputMode = .text - self.state?.updated(transition: .spring(duration: 0.4)) + if hasFirstResponder(self) || self.sendMessageContext.currentInputMode == .media { + if let view = self.inputPanel.view as? MessageInputPanelComponent.View { + if view.canDeactivateInput() { + self.sendMessageContext.currentInputMode = .text + if hasFirstResponder(self) { + view.deactivateInput() + } else { + self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged))) + } + } else { + view.animateError() + } + } } else if self.displayViewList { let point = recognizer.location(in: self) @@ -1686,6 +1697,7 @@ public final class StoryItemSetContainerComponent: Component { strings: component.strings, style: .story, placeholder: "Reply Privately...", + maxLength: 4096, queryTypes: [.mention, .emoji], alwaysDarkWhenHasText: component.metrics.widthClass == .regular, nextInputMode: { [weak self] hasText in @@ -2538,11 +2550,29 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext.openPeerMention(view: self, peerId: peerId) case let .hashtag(username, value): self.sendMessageContext.openHashtag(view: self, hashtag: value, peerName: username) - case let .bankCard(value): - let _ = value + case .bankCard: + break case .customEmoji: break } + }, + longTapAction: { [weak self] action in + guard let self, let component = self.component else { + return + } + self.sendMessageContext.presentTextEntityActions(view: self, action: action, openUrl: { [weak self] url, concealed in + openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in + guard let self, let component = self.component, let controller = component.controller() else { + return + } + controller.present(c, in: .window(.root)) + }, openResolved: { [weak self] resolved in + guard let self else { + return + } + self.sendMessageContext.openResolved(view: self, result: resolved, forceExternal: false, concealed: concealed) + }) + }) } )), environment: {}, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 49f271b36a..5939dd0847 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -36,6 +36,8 @@ import OverlayStatusController import PresentationDataUtils import TextFieldComponent import StickerPackPreviewUI +import OpenInExternalAppUI +import SafariServices final class StoryItemSetContainerSendMessage { enum InputMode { @@ -526,7 +528,7 @@ final class StoryItemSetContainerSendMessage { if self.videoRecorderValue == nil { if let currentInputPanelFrame = view.inputPanel.view?.frame { self.videoRecorder.set(.single(legacyInstantVideoController(theme: defaultDarkPresentationTheme, forStory: true, panelFrame: view.convert(currentInputPanelFrame, to: nil), context: component.context, peerId: peer.id, slowmodeState: nil, hasSchedule: true, send: { [weak self, weak view] videoController, message in - guard let self, let view, let component = view.component else { + guard let self, let view else { return } guard let message = message else { @@ -541,15 +543,6 @@ final class StoryItemSetContainerSendMessage { self.videoRecorder.set(.single(nil)) self.sendMessages(view: view, peer: peer, messages: [updatedMessage]) - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - view.component?.controller()?.present(UndoOverlayController( - presentationData: presentationData, - content: .succeed(text: "Message Sent"), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), in: .current) }, displaySlowmodeTooltip: { [weak self] view, rect in //self?.interfaceInteraction?.displaySlowmodeTooltip(view, rect) let _ = self @@ -597,15 +590,6 @@ final class StoryItemSetContainerSendMessage { self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) HapticFeedback().tap() - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - view.component?.controller()?.present(UndoOverlayController( - presentationData: presentationData, - content: .succeed(text: "Message Sent"), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), in: .current) } }) } else if let videoRecorderValue = self.videoRecorderValue { @@ -2520,4 +2504,97 @@ final class StoryItemSetContainerSendMessage { } })) } + + func presentTextEntityActions(view: StoryItemSetContainerComponent.View, action: StoryContentCaptionComponent.Action, openUrl: @escaping (String, Bool) -> Void) { + guard let component = view.component else { + return + } + + let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: component.theme, fontSize: .regular), allowInputInset: false) + + var canOpenIn = false + + let title: String + let value: String + var openAction: String? = component.strings.Conversation_LinkDialogOpen + var copyAction = component.strings.Conversation_ContextMenuCopy + switch action { + case let .url(url, _): + title = url + value = url + canOpenIn = availableOpenInOptions(context: component.context, item: .url(url: url)).count > 1 + if canOpenIn { + openAction = component.strings.Conversation_FileOpenIn + } + copyAction = component.strings.Conversation_ContextMenuCopyLink + case let .hashtag(_, hashtag): + title = hashtag + value = hashtag + case let .bankCard(bankCard): + title = bankCard + value = bankCard + openAction = nil + case let .peerMention(_, mention): + title = mention + value = mention + case let .textMention(mention): + title = mention + value = mention + case .customEmoji: + return + } + + var items: [ActionSheetItem] = [] + items.append(ActionSheetTextItem(title: title)) + + if let openAction { + items.append(ActionSheetButtonItem(title: openAction, color: .accent, action: { [weak self, weak view, weak actionSheet] in + actionSheet?.dismissAnimated() + if let self, let view { + switch action { + case let .url(url, concealed): + if canOpenIn { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = OpenInActionSheetController(context: component.context, item: .url(url: url), openUrl: { url in + if let navigationController = component.controller()?.navigationController as? NavigationController { + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } + }) + component.controller()?.present(actionSheet, in: .window(.root)) + } else { + openUrl(url, concealed) + } + case let .hashtag(peerName, value): + self.openHashtag(view: view, hashtag: value, peerName: peerName) + case let .peerMention(peerId, _): + self.openPeerMention(view: view, peerId: peerId) + case let .textMention(mention): + self.openPeerMention(view: view, name: mention) + case .customEmoji, .bankCard: + return + } + } + })) + } + + items.append(ActionSheetButtonItem(title: copyAction, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = value + })) + + if case let .url(url, _) = action, let link = URL(string: url) { + items.append(ActionSheetButtonItem(title: component.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + })) + } + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: component.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + + component.controller()?.present(actionSheet, in: .window(.root)) + } } diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index eb33df920e..947c316756 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -24,6 +24,7 @@ public final class TextFieldComponent: Component { public final class ExternalState { public fileprivate(set) var isEditing: Bool = false public fileprivate(set) var hasText: Bool = false + public fileprivate(set) var textLength: Int = 0 public var initialText: NSAttributedString? public var hasTrackingView = false @@ -492,6 +493,10 @@ public final class TextFieldComponent: Component { self.textView.becomeFirstResponder() } + public func deactivateInput() { + self.textView.resignFirstResponder() + } + private var spoilersRevealed = false private var spoilerIsDisappearing = false private func updateSpoilersRevealed(animated: Bool = true) { @@ -775,6 +780,7 @@ public final class TextFieldComponent: Component { component.externalState.hasText = self.textStorage.length != 0 component.externalState.isEditing = isEditing + component.externalState.textLength = self.textStorage.string.count if component.hideKeyboard { if self.textView.inputView == nil {