From 4e0da9eaa4e408bb4ddf7b58ee22112c43a979b7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 9 Sep 2023 23:10:19 +0400 Subject: [PATCH] Cherry-pick various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 9 + .../Sources/AccountContext.swift | 2 +- ...AttachmentTextInputActionButtonsNode.swift | 1 + .../AttachmentTextInputPanelNode.swift | 20 +- .../Sources/AttachmentController.swift | 16 +- .../Sources/AttachmentPanel.swift | 30 +-- ...ChatSendMessageActionSheetController.swift | 7 +- .../ChatItemGalleryFooterContentNode.swift | 6 +- .../Items/UniversalVideoGalleryItem.swift | 8 +- .../SecretMediaPreviewController.swift | 97 ++++++---- .../TGMediaPickerGalleryInterfaceView.h | 2 + .../TGPhotoCaptionInputMixin.h | 2 +- .../TGPhotoPaintStickersContext.h | 4 +- .../TGMediaPickerGalleryInterfaceView.m | 9 +- .../Sources/TGPhotoCaptionInputMixin.m | 19 +- .../Sources/LegacyMediaPickerGallery.swift | 180 +++++++++--------- .../Sources/MediaPickerScreen.swift | 9 +- .../Sources/MediaPickerTitleView.swift | 1 + .../Sources/ChannelAdminsController.swift | 2 +- .../Sources/RadialStatusIconContentNode.swift | 13 +- .../Sources/RadialStatusNode.swift | 6 +- ...RadialStatusSecretTimeoutContentNode.swift | 46 ++++- .../DataPrivacySettingsController.swift | 6 +- submodules/StatisticsUI/BUILD | 1 + .../PendingMessageUploadedContent.swift | 45 ++++- .../Messages/AttachMenuBots.swift | 49 ++++- .../Messages/TelegramEngineMessages.swift | 4 + .../ChatEntityKeyboardInputNode/BUILD | 2 +- .../Sources/ChatEntityKeyboardInputNode.swift | 3 +- .../Components/LegacyMessageInputPanel/BUILD | 2 + .../Sources/LegacyMessageInputPanel.swift | 106 +++++++++-- .../LegacyMessageInputPanelInputView/BUILD | 19 ++ .../LegacyMessageInputPanelInputView.swift | 11 ++ .../Sources/MediaEditorScreen.swift | 1 + .../Sources/StoryPreviewComponent.swift | 1 + .../Sources/MessageInputPanelComponent.swift | 7 + .../Sources/StorageUsageScreen.swift | 2 +- .../StoryItemSetContainerComponent.swift | 1 + ...StoryItemSetContainerViewSendMessage.swift | 16 +- .../Sources/TextFieldComponent.swift | 24 ++- .../TelegramUI/Sources/ChatController.swift | 74 +++++-- .../ChatInterfaceStateContextMenus.swift | 2 +- .../Sources/ChatMessageBubbleItemNode.swift | 5 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 18 +- .../PeerInfoScreenDisclosureItem.swift | 38 +++- .../Sources/PeerInfo/PeerInfoData.swift | 21 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 88 +++++++-- .../Sources/SharedAccountContext.swift | 104 +--------- .../Sources/UndoOverlayController.swift | 2 +- .../Sources/UndoOverlayControllerNode.swift | 4 +- submodules/WebUI/BUILD | 1 + .../WebUI/Sources/WebAppController.swift | 81 +++++--- .../Sources/WebAppTermsAlertController.swift | 149 ++++++++++++--- 53 files changed, 930 insertions(+), 446 deletions(-) create mode 100644 submodules/TelegramUI/Components/LegacyMessageInputPanelInputView/BUILD create mode 100644 submodules/TelegramUI/Components/LegacyMessageInputPanelInputView/Sources/LegacyMessageInputPanelInputView.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 82ea8cbc20..e536fafcf2 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9899,3 +9899,12 @@ Sorry for the inconvenience."; "SessionReview.Title" = "New Login Prevented"; "SessionReview.Text" = "We have terminated the login attempt from **%1$@**, **%2$@**"; "SessionReview.OkAction" = "Got it"; + +"WebApp.DisclaimerTitle" = "Terms of Use"; +"WebApp.DisclaimerText" = "You are about to use a mini app operated by an independent party not affiliated with Telegram. You must agree to the Terms of Use of mini apps to continue."; +"WebApp.DisclaimerAgree" = "I agree to the [Terms of Use]()"; +"WebApp.DisclaimerContinue" = "Continue"; +"WebApp.Disclaimer_URL" = "https://telegram.org/tos/mini-apps"; + +"WebApp.ShortcutsAdded" = "**%@** shortcut added in attachment menu."; +"WebApp.ShortcutsSettingsAdded" = "**%@** shortcut added in attachment menu and Settings."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index bd236d0333..81817ac21b 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -869,7 +869,7 @@ public protocol SharedAccountContext: AnyObject { func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController func makeStorageManagementController(context: AccountContext) -> ViewController func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController - func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? + func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController func makeArchiveSettingsController(context: AccountContext) -> ViewController diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift index b52a183491..10fa313b6e 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift @@ -82,6 +82,7 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode { super.didLoad() let gestureRecognizer = ContextGesture(target: nil, action: nil) + gestureRecognizer.isEnabled = self.sendButtonLongPressEnabled self.gestureRecognizer = gestureRecognizer self.sendButton.view.addGestureRecognizer(gestureRecognizer) gestureRecognizer.activated = { [weak self] recognizer, _ in diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index ef2e88af01..dc68e2f27f 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -327,7 +327,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS private var maxCaptionLength: Int32? - public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, presentController: @escaping (ViewController) -> Void, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { + public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, isScheduledMessages: Bool = false, presentController: @escaping (ViewController) -> Void, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { self.context = context self.presentationInterfaceState = presentationInterfaceState self.isCaption = isCaption @@ -374,8 +374,13 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS super.init() - self.actionButtons.sendButtonLongPressed = { [weak self] node, gesture in - self?.interfaceInteraction?.displaySendMessageOptions(node, gesture) + if !isScheduledMessages { + self.actionButtons.sendButtonLongPressed = { [weak self] node, gesture in + self?.interfaceInteraction?.displaySendMessageOptions(node, gesture) + } + self.actionButtons.sendButtonLongPressEnabled = true + } else { + self.actionButtons.sendButtonLongPressEnabled = false } self.actionButtons.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside) @@ -461,7 +466,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } - public func setTimeout(_ timeout: Int32) { + public func setTimeout(_ timeout: Int32, isVideo: Bool) { } public func animate(_ view: UIView, frame: CGRect) { @@ -471,8 +476,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS public func onAnimateOut() { } - public func dismissInput() { + public func dismissInput() -> Bool { self.ensureUnfocused() + return true } public func baseHeight() -> CGFloat { @@ -727,8 +733,6 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize) } - - self.actionButtons.sendButtonLongPressEnabled = true } let sendButtonHasApplyIcon = self.isCaption || interfaceState.interfaceState.editMessage != nil @@ -1692,7 +1696,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } if let sendPressed = self.sendPressed, let presentationInterfaceState = self.effectivePresentationInterfaceState?() { - self.dismissInput() + let _ = self.dismissInput() let effectiveInputText = presentationInterfaceState.interfaceState.composeInputState.inputText sendPressed(effectiveInputText) return diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 4b35a35891..eea8063153 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -19,7 +19,7 @@ public enum AttachmentButtonType: Equatable { case location case contact case poll - case app(EnginePeer, String, [AttachMenuBots.Bot.IconName: TelegramMediaFile]) + case app(AttachMenuBot) case gift case standalone @@ -55,8 +55,8 @@ public enum AttachmentButtonType: Equatable { } else { return false } - case let .app(lhsPeer, lhsTitle, lhsIcons): - if case let .app(rhsPeer, rhsTitle, rhsIcons) = rhs, lhsPeer == rhsPeer, lhsTitle == rhsTitle, lhsIcons == rhsIcons { + case let .app(lhsBot): + if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id { return true } else { return false @@ -179,6 +179,7 @@ public class AttachmentController: ViewController { private let context: AccountContext private let updatedPresentationData: (initial: PresentationData, signal: Signal)? private let chatLocation: ChatLocation? + private let isScheduledMessages: Bool private let buttons: [AttachmentButtonType] private let initialButton: AttachmentButtonType private let fromMenu: Bool @@ -294,7 +295,7 @@ public class AttachmentController: ViewController { self.container = AttachmentContainer() self.container.canHaveKeyboardFocus = true - self.panel = AttachmentPanel(context: controller.context, chatLocation: controller.chatLocation, updatedPresentationData: controller.updatedPresentationData, makeEntityInputView: makeEntityInputView) + self.panel = AttachmentPanel(context: controller.context, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, updatedPresentationData: controller.updatedPresentationData, makeEntityInputView: makeEntityInputView) self.panel.fromMenu = controller.fromMenu self.panel.isStandalone = controller.isStandalone @@ -445,9 +446,9 @@ public class AttachmentController: ViewController { if let controller = self.controller { let _ = self.switchToController(controller.initialButton) - if case let .app(bot, _, _) = controller.initialButton { + if case let .app(bot) = controller.initialButton { if let index = controller.buttons.firstIndex(where: { - if case let .app(otherBot, _, _) = $0, otherBot.id == bot.id { + if case let .app(otherBot) = $0, otherBot.peer.id == bot.peer.id { return true } else { return false @@ -907,10 +908,11 @@ public class AttachmentController: ViewController { public var getSourceRect: (() -> CGRect?)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation?, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil}) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation?, isScheduledMessages: Bool = false, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil}) { self.context = context self.updatedPresentationData = updatedPresentationData self.chatLocation = chatLocation + self.isScheduledMessages = isScheduledMessages self.buttons = buttons self.initialButton = initialButton self.fromMenu = fromMenu diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 505cd6b429..766a54d4de 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -200,15 +200,15 @@ private final class AttachButtonComponent: CombinedComponent { case .gift: name = strings.Attachment_Gift imageName = "Chat/Attach Menu/Gift" - case let .app(peer, appName, appIcons): - botPeer = peer - name = appName + case let .app(bot): + botPeer = bot.peer + name = bot.shortName imageName = "" - if let file = appIcons[.iOSAnimated] { + if let file = bot.icons[.iOSAnimated] { animationFile = file - } else if let file = appIcons[.iOSStatic] { + } else if let file = bot.icons[.iOSStatic] { imageFile = file - } else if let file = appIcons[.default] { + } else if let file = bot.icons[.default] { imageFile = file } case .standalone: @@ -725,6 +725,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { private let context: AccountContext + private let isScheduledMessages: Bool private var presentationData: PresentationData private var presentationDataDisposable: Disposable? @@ -775,9 +776,10 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { var mainButtonPressed: () -> Void = { } - init(context: AccountContext, chatLocation: ChatLocation?, updatedPresentationData: (initial: PresentationData, signal: Signal)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { + init(context: AccountContext, chatLocation: ChatLocation?, isScheduledMessages: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { self.context = context self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + self.isScheduledMessages = isScheduledMessages self.makeEntityInputView = makeEntityInputView @@ -1140,10 +1142,10 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { } let type = self.buttons[i] - if case let .app(peer, _, iconFiles) = type { - for (name, file) in iconFiles { - if [.default, .iOSAnimated, .placeholder].contains(name) { - if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(peer._asPeer()) { + if case let .app(bot) = type { + for (name, file) in bot.icons { + if [.default, .iOSAnimated, .iOSSettingsStatic, .placeholder].contains(name) { + if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(bot.peer._asPeer()) { if case .placeholder = name { let account = self.context.account let path = account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation()) @@ -1212,8 +1214,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { accessibilityTitle = self.presentationData.strings.Attachment_Poll case .gift: accessibilityTitle = self.presentationData.strings.Attachment_Gift - case let .app(_, appName, _): - accessibilityTitle = appName + case let .app(bot): + accessibilityTitle = bot.shortName case .standalone: accessibilityTitle = "" } @@ -1248,7 +1250,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { private func loadTextNodeIfNeeded() { if let _ = self.textInputPanelNode { } else { - let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, isAttachment: true, presentController: { [weak self] c in + let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, isAttachment: true, isScheduledMessages: self.isScheduledMessages, presentController: { [weak self] c in if let strongSelf = self { strongSelf.present(c) } diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index cb71a621da..ac5f097cc3 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -22,6 +22,7 @@ public final class ChatSendMessageActionSheetController: ViewController { private let context: AccountContext private let peerId: EnginePeer.Id? + private let isScheduledMessages: Bool private let forwardMessageIds: [EngineMessage.Id]? private let hasEntityKeyboard: Bool @@ -45,9 +46,10 @@ public final class ChatSendMessageActionSheetController: ViewController { public var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode) -> Void, schedule: @escaping () -> Void) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, isScheduledMessages: Bool = false, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode) -> Void, schedule: @escaping () -> Void) { self.context = context self.peerId = peerId + self.isScheduledMessages = isScheduledMessages self.forwardMessageIds = forwardMessageIds self.hasEntityKeyboard = hasEntityKeyboard self.gesture = gesture @@ -101,6 +103,9 @@ public final class ChatSendMessageActionSheetController: ViewController { isSecret = peerId.namespace == Namespaces.Peer.SecretChat canSchedule = !isSecret } + if self.isScheduledMessages { + canSchedule = false + } self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, presentationData: self.presentationData, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputNode: self.textInputNode, attachment: self.attachment, canSendWhenOnline: self.canSendWhenOnline, forwardedCount: forwardedCount, hasEntityKeyboard: self.hasEntityKeyboard, emojiViewProvider: self.emojiViewProvider, send: { [weak self] in self?.sendMessage(.generic) diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 675f3aecf5..0d4b651228 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -631,7 +631,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false) { self.currentMessage = message - let canDelete: Bool + var canDelete: Bool var canShare = !message.containsSecretMedia var canFullscreen = false @@ -715,6 +715,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll canEdit = false } + if message.containsSecretMedia { + canDelete = false + } + var authorNameText: String? if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature { authorNameText = authorSignature diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index c615262a11..a58340a620 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1344,7 +1344,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.isPlayingPromise.set(false) strongSelf.isPlaying = false if strongSelf.isCentral == true { - strongSelf.updateControlsVisibility(true) + if !item.isSecret { + strongSelf.updateControlsVisibility(true) + } } } } @@ -1481,7 +1483,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if strongSelf.actionAtEnd == .stop && strongSelf.isCentral == true { strongSelf.isPlayingPromise.set(false) strongSelf.isPlaying = false - strongSelf.updateControlsVisibility(true) + if !item.isSecret { + strongSelf.updateControlsVisibility(true) + } } } } diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 38be8215c6..bbdc4dab0e 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -60,16 +60,20 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode { private var validLayout: (ContainerViewLayout, CGFloat)? - var beginTimeAndTimeout: (Double, Double)? { + var beginTimeAndTimeout: (Double, Double, Bool)? { didSet { - if let (beginTime, timeout) = self.beginTimeAndTimeout { + if let (beginTime, timeout, isOutgoing) = self.beginTimeAndTimeout { var beginTime = beginTime if self.timeoutNode == nil { let timeoutNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) self.timeoutNode = timeoutNode let icon: RadialStatusNodeState.SecretTimeoutIcon let timeoutValue = Int32(timeout) - if timeoutValue == viewOnceTimeout { + + let state: RadialStatusNodeState + if timeoutValue == 0 && isOutgoing { + state = .staticTimeout + } else if timeoutValue == viewOnceTimeout { beginTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 if let image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ViewOnce"), color: .white) { @@ -77,10 +81,11 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode { } else { icon = .flame } + state = .secretTimeout(color: .white, icon: icon, beginTime: beginTime, timeout: timeout, sparks: isOutgoing ? false : true) } else { - icon = .flame + state = .secretTimeout(color: .white, icon: .flame, beginTime: beginTime, timeout: timeout, sparks: true) } - timeoutNode.transitionToState(.secretTimeout(color: .white, icon: icon, beginTime: beginTime, timeout: timeout, sparks: true), completion: {}) + timeoutNode.transitionToState(state, completion: {}) self.addSubnode(timeoutNode) timeoutNode.addTarget(self, action: #selector(self.statusTapGesture), forControlEvents: .touchUpInside) @@ -103,6 +108,8 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode { } } + var onDismissTransitionUpdate: (CGFloat) -> Void = { _ in } + override func animateIn(animateContent: Bool, useSimpleAnimation: Bool) { super.animateIn(animateContent: animateContent, useSimpleAnimation: useSimpleAnimation) @@ -119,6 +126,7 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode { override func updateDismissTransition(_ value: CGFloat) { self.timeoutNode?.alpha = value + self.onDismissTransitionUpdate(value) } override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -157,6 +165,7 @@ public final class SecretMediaPreviewController: ViewController { private var currentNodeMessageId: MessageId? private var currentNodeMessageIsVideo = false private var currentNodeMessageIsViewOnce = false + private var currentMessageIsDismissed = false private var tempFile: TempBoxFile? private let centralItemAttributesDisposable = DisposableSet(); @@ -252,6 +261,12 @@ public final class SecretMediaPreviewController: ViewController { } } + self.controllerNode.onDismissTransitionUpdate = { [weak self] _ in + if let self { + self.dismissAllTooltips() + } + } + self.controllerNode.statusBar = self.statusBar self.controllerNode.navigationBar = self.navigationBar @@ -270,11 +285,6 @@ public final class SecretMediaPreviewController: ViewController { self.controllerNode.dismiss = { [weak self] in self?._hiddenMedia.set(.single(nil)) self?.presentingViewController?.dismiss(animated: false, completion: nil) - - if let tooltipController = self?.tooltipController { - self?.tooltipController = nil - tooltipController.dismiss() - } } self.controllerNode.beginCustomDismiss = { [weak self] _ in @@ -298,7 +308,7 @@ public final class SecretMediaPreviewController: ViewController { var hiddenItem: (MessageId, Media)? if let _ = index { if let message = strongSelf.messageView?.message, let media = mediaForMessage(message: message) { - var beginTimeAndTimeout: (Double, Double)? + var beginTimeAndTimeout: (Double, Double, Bool)? var videoDuration: Double? for media in message.media { if let file = media as? TelegramMediaFile { @@ -306,26 +316,34 @@ public final class SecretMediaPreviewController: ViewController { } } + var timerStarted = false + let isOutgoing = !message.flags.contains(.Incoming) if let attribute = message.autoclearAttribute { strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout if let countdownBeginTime = attribute.countdownBeginTime { + timerStarted = true if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout { - beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) + beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, max(videoDuration, Double(attribute.timeout)), isOutgoing) } else { - beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) + beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout), isOutgoing) } + } else if isOutgoing { + beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing) } } else if let attribute = message.autoremoveAttribute { strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout if let countdownBeginTime = attribute.countdownBeginTime { + timerStarted = true if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout { - beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) + beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, max(videoDuration, Double(attribute.timeout)), isOutgoing) } else { - beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) + beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout), isOutgoing) } - } + } else if isOutgoing { + beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing) + } } if let file = media as? TelegramMediaFile { @@ -350,12 +368,12 @@ public final class SecretMediaPreviewController: ViewController { strongSelf.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout } - if strongSelf.currentNodeMessageIsVideo { + if message.flags.contains(.Incoming) || strongSelf.currentNodeMessageIsVideo { if let node = strongSelf.controllerNode.pager.centralItemNode() { strongSelf.footerContentNode.set(node.footerContent()) } - } else if !message.flags.contains(.Incoming) { - if let _ = beginTimeAndTimeout { + } else { + if timerStarted { strongSelf.controllerNode.updatePresentationState({ $0.withUpdatedFooterContentNode(nil) }, transition: .immediate) @@ -448,10 +466,7 @@ public final class SecretMediaPreviewController: ViewController { } private func dismiss(forceAway: Bool) { - if let tooltipController = self.tooltipController { - self.tooltipController = nil - tooltipController.dismiss() - } + self.dismissAllTooltips() var animatedOutNode = true var animatedOutInterface = false @@ -494,6 +509,7 @@ public final class SecretMediaPreviewController: ViewController { if self.currentNodeMessageId != message.id { self.currentNodeMessageId = message.id var tempFilePath: String? + var duration: Double = 0.0 for media in message.media { if let file = media as? TelegramMediaFile { if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) { @@ -502,13 +518,14 @@ public final class SecretMediaPreviewController: ViewController { tempFilePath = tempFile.path self.currentNodeMessageIsVideo = true } + duration = file.duration ?? 0.0 break } } - + guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, isSecret: true, playbackRate: { nil }, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in if let self { - if self.currentNodeMessageIsViewOnce { + if self.currentNodeMessageIsViewOnce || (duration < 30.0 && !self.currentMessageIsDismissed) { if let node = self.controllerNode.pager.centralItemNode() as? UniversalVideoGalleryItemNode { node.seekToStart() } @@ -528,28 +545,34 @@ public final class SecretMediaPreviewController: ViewController { self._ready.set(ready |> map { true }) self.markMessageAsConsumedDisposable.set(self.context.engine.messages.markMessageContentAsConsumedInteractively(messageId: message.id).start()) } else { - var beginTimeAndTimeout: (Double, Double)? + var beginTimeAndTimeout: (Double, Double, Bool)? var videoDuration: Double? for media in message.media { if let file = media as? TelegramMediaFile { videoDuration = file.duration } } + + let isOutgoing = !message.flags.contains(.Incoming) if let attribute = message.autoclearAttribute { if let countdownBeginTime = attribute.countdownBeginTime { if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout { - beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) + beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, max(videoDuration, Double(attribute.timeout)), isOutgoing) } else { - beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) + beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout), isOutgoing) } + } else if isOutgoing { + beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing) } } else if let attribute = message.autoremoveAttribute { if let countdownBeginTime = attribute.countdownBeginTime { if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout { - beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) + beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, max(videoDuration, Double(attribute.timeout)), isOutgoing) } else { - beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) + beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout), isOutgoing) } + } else if isOutgoing { + beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing) } } @@ -566,6 +589,14 @@ public final class SecretMediaPreviewController: ViewController { if !(self.currentNodeMessageIsVideo || self.currentNodeMessageIsViewOnce) { self.dismiss() } + self.currentMessageIsDismissed = true + } + } + + private func dismissAllTooltips() { + if let tooltipController = self.tooltipController { + self.tooltipController = nil + tooltipController.dismiss() } } @@ -574,10 +605,7 @@ public final class SecretMediaPreviewController: ViewController { return } - if let tooltipController = self.tooltipController { - self.tooltipController = nil - tooltipController.dismiss() - } + self.dismissAllTooltips() let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 2.0), size: CGSize()) @@ -603,6 +631,7 @@ public final class SecretMediaPreviewController: ViewController { location: .point(location, .top), displayDuration: .default, inset: 8.0, + cornerRadius: 8.0, shouldDismissOnTouch: { _, _ in return .ignore } diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h index 696f9c9d92..ba7f57225c 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h @@ -54,6 +54,8 @@ - (void)editorTransitionIn; - (void)editorTransitionOut; +- (void)onDismiss; + - (void)setTabBarUserInteractionEnabled:(bool)enabled; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h index e420bc3634..fed6a04d4c 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h @@ -36,7 +36,7 @@ - (void)setCaption:(NSAttributedString *)caption animated:(bool)animated; - (void)setCaptionPanelHidden:(bool)hidden animated:(bool)animated; -- (void)setTimeout:(int32_t)timeout; +- (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo; - (void)updateLayoutWithFrame:(CGRect)frame edgeInsets:(UIEdgeInsets)edgeInsets animated:(bool)animated; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index eaf71260ae..61ae441820 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -22,11 +22,11 @@ @property (nonatomic, readonly) UIView * _Nonnull view; -- (void)setTimeout:(int32_t)timeout; +- (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo; - (NSAttributedString * _Nonnull)caption; - (void)setCaption:(NSAttributedString * _Nullable)caption; -- (void)dismissInput; +- (bool)dismissInput; - (void)animateView:(UIView * _Nonnull)view frame:(CGRect)frame; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m index 08ba0caa6a..177ec3ddbe 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -384,6 +384,8 @@ id galleryEditableItem = (id)strongSelf->_currentItem; [strongSelf->_editingContext setTimer:timeout forItem:galleryEditableItem.editableMediaItem]; + + [strongSelf->_selectionContext setItem:(id)galleryEditableItem.editableMediaItem selected:true animated:true sender:nil]; }; _captionMixin.stickersContext = stickersContext; @@ -836,7 +838,7 @@ id adjustments = dict[@"adjustments"]; NSNumber *timer = dict[@"timer"]; - [strongSelf->_captionMixin setTimeout:[timer intValue]]; + [strongSelf->_captionMixin setTimeout:[timer intValue] isVideo:editableMediaItem.isVideo]; if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]]) { @@ -1326,8 +1328,13 @@ [_captionMixin onAnimateOut]; } +- (void)onDismiss { + [_captionMixin onAnimateOut]; +} + - (void)setTransitionOutProgress:(CGFloat)transitionOutProgress manual:(bool)manual { + [_captionMixin onAnimateOut]; if (transitionOutProgress > FLT_EPSILON) [self setAllInterfaceHidden:true delay:0.0 animated:true]; else if (!manual) diff --git a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m index 08d8017a9f..1a0bc8ce62 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m +++ b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m @@ -141,8 +141,8 @@ [_inputPanel setCaption:caption]; } -- (void)setTimeout:(int32_t)timeout { - [_inputPanel setTimeout:timeout]; +- (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo { + [_inputPanel setTimeout:timeout isVideo:isVideo]; } - (void)setCaptionPanelHidden:(bool)hidden animated:(bool)__unused animated @@ -170,13 +170,14 @@ if (gestureRecognizer.state != UIGestureRecognizerStateRecognized) return; - _editing = false; - - [self.inputPanel dismissInput]; - [_dismissView removeFromSuperview]; - - if (self.finishedWithCaption != nil) - self.finishedWithCaption([_inputPanel caption]); + if ([self.inputPanel dismissInput]) { + _editing = false; + + [_dismissView removeFromSuperview]; + + if (self.finishedWithCaption != nil) + self.finishedWithCaption([_inputPanel caption]); + } } #pragma mark - Input Panel Delegate diff --git a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index 7604afc788..b19e46b3ac 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -100,7 +100,7 @@ enum LegacyMediaPickerGallerySource { case selection(item: TGMediaSelectableItem) } -func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, presentationData: PresentationData, source: LegacyMediaPickerGallerySource, immediateThumbnail: UIImage?, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, hasSilentPosting: Bool, hasSchedule: Bool, hasTimer: Bool, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?, @escaping () -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void, finishedTransitionIn: @escaping () -> Void, willTransitionOut: @escaping () -> Void, dismissAll: @escaping () -> Void) -> TGModernGalleryController { +func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, isScheduledMessages: Bool, presentationData: PresentationData, source: LegacyMediaPickerGallerySource, immediateThumbnail: UIImage?, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, hasSilentPosting: Bool, hasSchedule: Bool, hasTimer: Bool, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?, @escaping () -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void, finishedTransitionIn: @escaping () -> Void, willTransitionOut: @escaping () -> Void, dismissAll: @escaping () -> Void) -> TGModernGalleryController { let reminder = peer?.id == context.account.peerId let hasSilentPosting = hasSilentPosting && peer?.id != context.account.peerId @@ -224,103 +224,107 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, }) } } - model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model] item in - if let legacyController = legacyController, let item = item as? TGMediaPickerGalleryItem, let model = model, let selectionContext = selectionContext { - var effectiveHasSchedule = hasSchedule - - if let editingContext = editingContext { - if let timer = editingContext.timer(for: item.asset)?.intValue, timer > 0 { - effectiveHasSchedule = false - } - for item in selectionContext.selectedItems() { - if let editableItem = item as? TGMediaEditableItem, let timer = editingContext.timer(for: editableItem)?.intValue, timer > 0 { + if !isScheduledMessages { + model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model] item in + if let legacyController = legacyController, let item = item as? TGMediaPickerGalleryItem, let model = model, let selectionContext = selectionContext { + var effectiveHasSchedule = hasSchedule + + if let editingContext = editingContext { + if let timer = editingContext.timer(for: item.asset)?.intValue, timer > 0 { effectiveHasSchedule = false - break } - } - } - - let sendWhenOnlineAvailable: Signal - if let peer { - if case .secretChat = peer { - effectiveHasSchedule = false - } - sendWhenOnlineAvailable = context.account.viewTracker.peerView(peer.id) - |> take(1) - |> map { peerView -> Bool in - guard let peer = peerViewMainPeer(peerView) else { - return false - } - var sendWhenOnlineAvailable = false - if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status { - let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if currentTime > until { - sendWhenOnlineAvailable = true + for item in selectionContext.selectedItems() { + if let editableItem = item as? TGMediaEditableItem, let timer = editingContext.timer(for: editableItem)?.intValue, timer > 0 { + effectiveHasSchedule = false + break } } - if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { - sendWhenOnlineAvailable = false + } + + let sendWhenOnlineAvailable: Signal + if let peer { + if case .secretChat = peer { + effectiveHasSchedule = false } - return sendWhenOnlineAvailable - } - } else { - sendWhenOnlineAvailable = .single(false) - } - - let _ = (sendWhenOnlineAvailable - |> take(1) - |> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in - let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) - let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: false) - let dismissImpl = { [weak model] in - model?.dismiss(true, false) - dismissAll() - } - sheetController.send = { - completed(item.asset, false, nil, { - dismissImpl() - }) - } - sheetController.sendSilently = { - completed(item.asset, true, nil, { - dismissImpl() - }) - } - sheetController.sendWhenOnline = { - completed(item.asset, false, scheduleWhenOnlineTimestamp, { - dismissImpl() - }) - } - sheetController.schedule = { - presentSchedulePicker(true, { time in - completed(item.asset, false, time, { - dismissImpl() - }) - }) - } - sheetController.sendWithTimer = { - presentTimerPicker { time in - var items = selectionContext.selectedItems() ?? [] - items.append(item.asset as Any) - - for case let item as TGMediaEditableItem in items { - editingContext?.setTimer(time as NSNumber, for: item) + sendWhenOnlineAvailable = context.account.viewTracker.peerView(peer.id) + |> take(1) + |> map { peerView -> Bool in + guard let peer = peerViewMainPeer(peerView) else { + return false } - + var sendWhenOnlineAvailable = false + if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if currentTime > until { + sendWhenOnlineAvailable = true + } + } + if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { + sendWhenOnlineAvailable = false + } + return sendWhenOnlineAvailable + } + } else { + sendWhenOnlineAvailable = .single(false) + } + + let _ = (sendWhenOnlineAvailable + |> take(1) + |> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in + let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) + let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: false) + let dismissImpl = { [weak model] in + model?.dismiss(true, false) + dismissAll() + } + sheetController.send = { completed(item.asset, false, nil, { dismissImpl() }) } - } - sheetController.customDismissBlock = { [weak legacySheetController] in - legacySheetController?.dismiss() - } - legacySheetController.bind(controller: sheetController) - present(legacySheetController, nil) - - let hapticFeedback = HapticFeedback() - hapticFeedback.impact() - }) + sheetController.sendSilently = { [weak model] in + model?.interfaceView.onDismiss() + + completed(item.asset, true, nil, { + dismissImpl() + }) + } + sheetController.sendWhenOnline = { + completed(item.asset, false, scheduleWhenOnlineTimestamp, { + dismissImpl() + }) + } + sheetController.schedule = { + presentSchedulePicker(true, { time in + completed(item.asset, false, time, { + dismissImpl() + }) + }) + } + sheetController.sendWithTimer = { + presentTimerPicker { time in + var items = selectionContext.selectedItems() ?? [] + items.append(item.asset as Any) + + for case let item as TGMediaEditableItem in items { + editingContext?.setTimer(time as NSNumber, for: item) + } + + completed(item.asset, false, nil, { + dismissImpl() + }) + } + } + sheetController.customDismissBlock = { [weak legacySheetController] in + legacySheetController?.dismiss() + } + legacySheetController.bind(controller: sheetController) + present(legacySheetController, nil) + + let hapticFeedback = HapticFeedback() + hapticFeedback.impact() + }) + } } } model.interfaceView.setThumbnailSignalForItem { item in diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 6269743010..45124bd75e 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -169,6 +169,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { fileprivate var interaction: MediaPickerInteraction? private let peer: EnginePeer? + private let isScheduledMessages: Bool private let threadTitle: String? private let chatLocation: ChatLocation? private let bannedSendPhotos: (Int32, Bool)? @@ -970,7 +971,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.openingMedia = true - self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, threadTitle: controller.threadTitle, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index, reversed: reversed), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: hasSchedule, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in + self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, threadTitle: controller.threadTitle, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index, reversed: reversed), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: hasSchedule, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in self?.hiddenMediaId.set(.single(id)) }, initialLayout: layout, transitionHostView: { [weak self] in return self?.gridNode.view @@ -1009,7 +1010,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } self.openingMedia = true - self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, threadTitle: controller.threadTitle, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .selection(item: item), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in + self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, threadTitle: controller.threadTitle, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, presentationData: self.presentationData, source: .selection(item: item), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in self?.hiddenMediaId.set(.single(id)) }, initialLayout: layout, transitionHostView: { [weak self] in return self?.selectionNode?.view @@ -1524,6 +1525,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, + isScheduledMessages: Bool = false, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, subject: Subject, @@ -1541,6 +1543,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.peer = peer self.threadTitle = threadTitle self.chatLocation = chatLocation + self.isScheduledMessages = isScheduledMessages self.bannedSendPhotos = bannedSendPhotos self.bannedSendVideos = bannedSendVideos self.subject = subject @@ -2105,7 +2108,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { var updateNavigationStackImpl: ((AttachmentContainable) -> Void)? let groupsController = MediaGroupsScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, mediaAssetsContext: self.controllerNode.mediaAssetsContext, embedded: embedded, openGroup: { [weak self] collection in if let strongSelf = self { - let mediaPicker = MediaPickerScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: strongSelf.peer, threadTitle: strongSelf.threadTitle, chatLocation: strongSelf.chatLocation, bannedSendPhotos: strongSelf.bannedSendPhotos, bannedSendVideos: strongSelf.bannedSendVideos, subject: .assets(collection, mode), editingContext: strongSelf.interaction?.editingState, selectionContext: strongSelf.interaction?.selectionState) + let mediaPicker = MediaPickerScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: strongSelf.peer, threadTitle: strongSelf.threadTitle, chatLocation: strongSelf.chatLocation, isScheduledMessages: strongSelf.isScheduledMessages, bannedSendPhotos: strongSelf.bannedSendPhotos, bannedSendVideos: strongSelf.bannedSendVideos, subject: .assets(collection, mode), editingContext: strongSelf.interaction?.editingState, selectionContext: strongSelf.interaction?.selectionState) mediaPicker.presentSchedulePicker = strongSelf.presentSchedulePicker mediaPicker.presentTimerPicker = strongSelf.presentTimerPicker diff --git a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift index eb42c6da28..2cae7588f2 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift @@ -50,6 +50,7 @@ final class MediaPickerTitleView: UIView { transition.updateAlpha(node: self.arrowNode, alpha: self.segmentsHidden ? 1.0 : 0.0) transition.updateAlpha(node: self.segmentedControlNode, alpha: self.segmentsHidden ? 0.0 : 1.0) self.segmentedControlNode.isUserInteractionEnabled = !self.segmentsHidden + self.buttonNode.isUserInteractionEnabled = self.isEnabled && self.segmentsHidden } } } diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift index 50dc14e4e8..afc2bb66c6 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift @@ -581,7 +581,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation guard let peer = peer, let user = user else { return } - presentControllerImpl?(UndoOverlayController(presentationData: context.sharedContext.currentPresentationData.with { $0 }, content: .succeed(text: presentationData.strings.Channel_OwnershipTransfer_TransferCompleted(user.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string), elevatedLayout: false, action: { _ in return false }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: context.sharedContext.currentPresentationData.with { $0 }, content: .succeed(text: presentationData.strings.Channel_OwnershipTransfer_TransferCompleted(user.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, timeout: nil), elevatedLayout: false, action: { _ in return false }), nil) }) } diff --git a/submodules/RadialStatusNode/Sources/RadialStatusIconContentNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusIconContentNode.swift index 0d1510195a..794d1f3af1 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusIconContentNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusIconContentNode.swift @@ -21,7 +21,7 @@ private final class RadialStatusIconContentNodeParameters: NSObject { } final class RadialStatusIconContentNode: RadialStatusContentNode { - private let icon: RadialStatusIcon + let icon: RadialStatusIcon private var animationNode: FireIconNode? @@ -35,7 +35,7 @@ final class RadialStatusIconContentNode: RadialStatusContentNode { self.isOpaque = false if case .timeout = icon { - let animationNode = FireIconNode() + let animationNode = FireIconNode(animate: true) self.animationNode = animationNode self.addSubnode(animationNode) } @@ -44,7 +44,14 @@ final class RadialStatusIconContentNode: RadialStatusContentNode { override func layout() { super.layout() - self.animationNode?.frame = CGRect(x: 6.0, y: 2.0, width: 36.0, height: 36.0) + var factor: CGFloat = 0.75 + var offset: CGFloat = 0.0415 + if self.bounds.width < 30.0 { + factor = 1.0 + offset = 0.0 + } + let size = floorToScreenPixels(self.bounds.width * factor) + self.animationNode?.frame = CGRect(x: floorToScreenPixels((self.bounds.width - size) / 2.0), y: ceil(self.bounds.height * offset), width: size, height: size) } override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { diff --git a/submodules/RadialStatusNode/Sources/RadialStatusNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusNode.swift index 1b7d5e95dd..2aa3acba77 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusNode.swift @@ -224,7 +224,11 @@ public enum RadialStatusNodeState: Equatable { case .staticTimeout: return RadialStatusIconContentNode(icon: .timeout, synchronous: synchronous) case let .secretTimeout(color, icon, beginTime, timeout, sparks): - return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon, sparks: sparks) + var animate = true + if let current = current as? RadialStatusIconContentNode, case .timeout = current.icon { + animate = false + } + return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon, sparks: sparks, animate: animate) } } } diff --git a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift index bd2ec3b6ff..03133297ea 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift @@ -29,13 +29,15 @@ private final class RadialStatusSecretTimeoutContentNodeParameters: NSObject { let progress: CGFloat let sparks: Bool let particles: [ContentParticle] + let alphaProgress: CGFloat - init(color: UIColor, icon: RadialStatusNodeState.SecretTimeoutIcon, progress: CGFloat, sparks: Bool, particles: [ContentParticle]) { + init(color: UIColor, icon: RadialStatusNodeState.SecretTimeoutIcon, progress: CGFloat, sparks: Bool, particles: [ContentParticle], alphaProgress: CGFloat) { self.color = color self.icon = icon self.progress = progress self.sparks = sparks self.particles = particles + self.alphaProgress = alphaProgress } } @@ -51,14 +53,17 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { private let icon: RadialStatusNodeState.SecretTimeoutIcon private let sparks: Bool + private var animationBeginTime: Double? + private var progress: CGFloat = 0.0 + private var alphaProgress: CGFloat = 0.0 private var particles: [ContentParticle] = [] - private let animationNode = FireIconNode() + private var animationNode: FireIconNode? private var displayLink: CADisplayLink? - init(color: UIColor, beginTime: Double, timeout: Double, icon: RadialStatusNodeState.SecretTimeoutIcon, sparks: Bool) { + init(color: UIColor, beginTime: Double, timeout: Double, icon: RadialStatusNodeState.SecretTimeoutIcon, sparks: Bool, animate: Bool = true) { self.color = color self.beginTime = beginTime self.timeout = timeout @@ -85,7 +90,13 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { self.displayLink?.add(to: RunLoop.main, forMode: .common) if case .flame = icon { - self.addSubnode(self.animationNode) + if !animate { + self.animationBeginTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + } + + let animationNode = FireIconNode(animate: animate) + self.animationNode = animationNode + self.addSubnode(animationNode) } } @@ -103,7 +114,7 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { offset = 0.08 } let size = floorToScreenPixels(self.bounds.width * factor) - self.animationNode.frame = CGRect(x: floorToScreenPixels((self.bounds.width - size) / 2.0), y: ceil(self.bounds.height * offset), width: size, height: size) + self.animationNode?.frame = CGRect(x: floorToScreenPixels((self.bounds.width - size) / 2.0), y: ceil(self.bounds.height * offset), width: size, height: size) } override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) { @@ -132,12 +143,23 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { return } + let absoluteTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + + let alphaProgress: CGFloat + if let animationBeginTime = self.animationBeginTime { + let fadeInDuration: Double = 0.4 + alphaProgress = max(0.0, min(1.0, (absoluteTimestamp - animationBeginTime) / fadeInDuration)) + } else { + alphaProgress = 1.0 + } + var progress = min(1.0, CGFloat((absoluteTimestamp - self.beginTime) / self.timeout)) if self.timeout == 0x7fffffff { progress = 0.0 } self.progress = progress + self.alphaProgress = alphaProgress if self.sparks { let lineWidth: CGFloat = 1.75 @@ -193,7 +215,7 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { } override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return RadialStatusSecretTimeoutContentNodeParameters(color: self.color, icon: self.icon, progress: self.progress, sparks: self.sparks, particles: self.particles) + return RadialStatusSecretTimeoutContentNodeParameters(color: self.color, icon: self.icon, progress: self.progress, sparks: self.sparks, particles: self.particles, alphaProgress: self.alphaProgress) } @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { @@ -240,6 +262,8 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * parameters.progress if drawArc { + context.setAlpha(parameters.alphaProgress) + let path = CGMutablePath() path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) context.addPath(path) @@ -248,7 +272,7 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { for particle in parameters.particles { let size: CGFloat = 1.3 - context.setAlpha(particle.alpha) + context.setAlpha(particle.alpha * parameters.alphaProgress) context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size))) } } @@ -256,9 +280,13 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { } final class FireIconNode: ManagedAnimationNode { - init() { + init(animate: Bool) { super.init(size: CGSize(width: 100.0, height: 100.0)) - self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 0, endFrame: 120), duration: 2.0)) + if animate { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 0, endFrame: 120), duration: 2.0)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 120, endFrame: 120), duration: 0.001)) + } } } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift index 5227e16baa..855d3bfc45 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift @@ -377,7 +377,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { text = nil } if let text = text { - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: text), elevatedLayout: false, action: { _ in return false })) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: nil), elevatedLayout: false, action: { _ in return false })) } })) } @@ -426,7 +426,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { return state } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_ContactsReset_ContactsDeleted), elevatedLayout: false, action: { _ in return false })) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_ContactsReset_ContactsDeleted, timeout: nil), elevatedLayout: false, action: { _ in return false })) })) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})])) } @@ -476,7 +476,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { return state } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_DeleteDrafts_DraftsDeleted), elevatedLayout: false, action: { _ in return false })) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_DeleteDrafts_DraftsDeleted, timeout: nil), elevatedLayout: false, action: { _ in return false })) })) } dismissAction() diff --git a/submodules/StatisticsUI/BUILD b/submodules/StatisticsUI/BUILD index 299a082dee..3a79d9bcd3 100644 --- a/submodules/StatisticsUI/BUILD +++ b/submodules/StatisticsUI/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/ItemListPeerItem:ItemListPeerItem", "//submodules/ItemListPeerActionItem:ItemListPeerActionItem", "//submodules/ContextUI:ContextUI", + "//submodules/PremiumUI:PremiumUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index ccd562298c..8e2b195478 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -123,7 +123,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post } } } - return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, passFetchProgress: false, forceNoBigParts: false, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file) + return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, passFetchProgress: false, forceNoBigParts: false, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file) } else { if forceReupload { let mediaReference: AnyMediaReference @@ -157,7 +157,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil))) } } else { - return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file) + return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file) } } else if let contact = media as? TelegramMediaContact { let input = Api.InputMedia.inputMediaContact(phoneNumber: contact.phoneNumber, firstName: contact.firstName, lastName: contact.lastName, vcard: contact.vCardData ?? "") @@ -291,13 +291,17 @@ private func maybePredownloadedImageResource(postbox: Postbox, peerId: PeerId, r |> switchToLatest } -private func maybePredownloadedFileResource(postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, peerId: PeerId, resource: MediaResource, forceRefresh: Bool) -> Signal { +private func maybePredownloadedFileResource(postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, peerId: PeerId, resource: MediaResource, autoRemove: Bool, forceRefresh: Bool) -> Signal { if peerId.namespace == Namespaces.Peer.SecretChat { return .single(.none) } + if autoRemove { + return .single(.none) + } + #if DEBUG - if "".isEmpty { + if !"".isEmpty { return .single(.none) } #endif @@ -507,7 +511,13 @@ if "".isEmpty { if hasSpoiler { flags |= 1 << 1 } - return maybeCacheUploadedResource(postbox: postbox, key: referenceKey, result: .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaPhoto(flags: flags, id: .inputPhoto(id: id, accessHash: accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: nil)), media: mediaImage) + + let result: PendingMessageUploadedContentResult = .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaPhoto(flags: flags, id: .inputPhoto(id: id, accessHash: accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: nil)) + if let _ = ttlSeconds { + return .single(result) + } else { + return maybeCacheUploadedResource(postbox: postbox, key: referenceKey, result: result, media: mediaImage) + } } default: break @@ -656,14 +666,20 @@ public func statsCategoryForFileWithAttributes(_ attributes: [TelegramMediaFileA return .file } -private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], file: TelegramMediaFile) -> Signal { - return maybePredownloadedFileResource(postbox: postbox, auxiliaryMethods: auxiliaryMethods, peerId: peerId, resource: file.resource, forceRefresh: forceReupload) +private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, file: TelegramMediaFile) -> Signal { + return maybePredownloadedFileResource(postbox: postbox, auxiliaryMethods: auxiliaryMethods, peerId: peerId, resource: file.resource, autoRemove: autoremoveMessageAttribute != nil || autoclearMessageAttribute != nil, forceRefresh: forceReupload) |> mapToSignal { result -> Signal in var referenceKey: CachedSentMediaReferenceKey? switch result { case let .media(media, key): if !forceReupload, let file = media as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { var flags: Int32 = 0 + var ttlSeconds: Int32? + if let autoclearMessageAttribute = autoclearMessageAttribute { + flags |= 1 << 0 + ttlSeconds = autoclearMessageAttribute.timeout + } + for attribute in attributes { if let _ = attribute as? MediaSpoilerMessageAttribute { flags |= 1 << 2 @@ -672,7 +688,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili return .single(.progress(1.0)) |> then( - .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: nil, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) + .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) ) } referenceKey = key @@ -885,10 +901,21 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili case let .messageMediaDocument(_, document, _, _): if let document = document, let mediaFile = telegramMediaFileFromApiDocument(document), let resource = mediaFile.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { var flags: Int32 = 0 + var ttlSeconds: Int32? + if let autoclearMessageAttribute = autoclearMessageAttribute { + flags |= 1 << 0 + ttlSeconds = autoclearMessageAttribute.timeout + } if hasSpoiler { flags |= (1 << 2) } - return maybeCacheUploadedResource(postbox: postbox, key: referenceKey, result: .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaDocument(flags: flags, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: nil, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)), media: mediaFile) + + let result: PendingMessageUploadedContentResult = .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaDocument(flags: flags, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)) + if let _ = ttlSeconds { + return .single(result) + } else { + return maybeCacheUploadedResource(postbox: postbox, key: referenceKey, result: result, media: mediaFile) + } } default: break diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift index d77f5b2ab1..33861f0caa 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift @@ -18,7 +18,9 @@ public final class AttachMenuBots: Equatable, Codable { case `default` = 0 case iOSStatic case iOSAnimated + case iOSSettingsStatic case macOSAnimated + case macOSSettingsStatic case placeholder init?(string: String) { @@ -29,6 +31,10 @@ public final class AttachMenuBots: Equatable, Codable { self = .iOSStatic case "ios_animated": self = .iOSAnimated + case "ios_side_menu_static": + self = .iOSSettingsStatic + case "macos_side_menu_static": + self = .macOSSettingsStatic case "macos_animated": self = .macOSAnimated case "placeholder_static": @@ -55,6 +61,7 @@ public final class AttachMenuBots: Equatable, Codable { public static let showInAttachMenu = Flags(rawValue: 1 << 2) public static let showInSettings = Flags(rawValue: 1 << 3) public static let showInSettingsDisclaimer = Flags(rawValue: 1 << 4) + public static let notActivated = Flags(rawValue: 1 << 5) } public struct PeerFlags: OptionSet, Codable { @@ -190,6 +197,10 @@ public final class AttachMenuBots: Equatable, Codable { try container.encode(Int32(self.flags.rawValue), forKey: .flags) } + + func withUpdatedFlags(_ flags: Flags) -> Bot { + return Bot(peerId: self.peerId, name: self.name, icons: self.icons, peerTypes: self.peerTypes, flags: flags) + } } private enum CodingKeys: String, CodingKey { @@ -320,6 +331,9 @@ func managedSynchronizeAttachMenuBots(accountPeerId: PeerId, postbox: Postbox, n } } var flags: AttachMenuBots.Bot.Flags = [] + if (apiFlags & (1 << 0)) != 0 { + flags.insert(.notActivated) + } if (apiFlags & (1 << 1)) != 0 { flags.insert(.hasSettings) } @@ -412,6 +426,8 @@ func _internal_addBotToAttachMenu(accountPeerId: PeerId, postbox: Postbox, netwo } func _internal_removeBotFromAttachMenu(accountPeerId: PeerId, postbox: Postbox, network: Network, botId: PeerId) -> Signal { + let _ = removeCachedAttachMenuBot(postbox: postbox, botId: botId).start() + return postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(botId), let inputUser = apiInputUser(peer) else { return .complete() @@ -431,13 +447,30 @@ func _internal_removeBotFromAttachMenu(accountPeerId: PeerId, postbox: Postbox, |> afterCompleted { let _ = (managedSynchronizeAttachMenuBots(accountPeerId: accountPeerId, postbox: postbox, network: network, force: true) |> take(1)).start(completed: { - let _ = removeCachedAttachMenuBot(postbox: postbox, botId: botId) + let _ = removeCachedAttachMenuBot(postbox: postbox, botId: botId).start() }) } } |> switchToLatest } +func _internal_acceptAttachMenuBotDisclaimer(postbox: Postbox, botId: PeerId) -> Signal { + return postbox.transaction { transaction in + if let attachMenuBots = cachedAttachMenuBots(transaction: transaction) { + var updatedAttachMenuBots = attachMenuBots + if let index = attachMenuBots.bots.firstIndex(where: { $0.peerId == botId }) { + var updatedFlags = attachMenuBots.bots[index].flags + updatedFlags.remove(.showInSettingsDisclaimer) + let updatedBot = attachMenuBots.bots[index].withUpdatedFlags(updatedFlags) + var updatedBots = attachMenuBots.bots + updatedBots[index] = updatedBot + updatedAttachMenuBots = AttachMenuBots(hash: attachMenuBots.hash, bots: updatedBots) + } + setCachedAttachMenuBots(transaction: transaction, attachMenuBots: updatedAttachMenuBots) + } + } |> ignoreValues +} + public struct AttachMenuBot { public let peer: EnginePeer public let shortName: String @@ -445,7 +478,7 @@ public struct AttachMenuBot { public let peerTypes: AttachMenuBots.Bot.PeerFlags public let flags: AttachMenuBots.Bot.Flags - init(peer: EnginePeer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) { + public init(peer: EnginePeer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) { self.peer = peer self.shortName = shortName self.icons = icons @@ -590,6 +623,7 @@ public final class BotApp: Equatable, Codable { public static let notActivated = Flags(rawValue: 1 << 0) public static let requiresWriteAccess = Flags(rawValue: 1 << 1) + public static let hasSettings = Flags(rawValue: 1 << 2) } public let id: Int64 @@ -718,7 +752,7 @@ func _internal_getBotApp(account: Account, reference: BotAppReference) -> Signal } |> mapToSignal { result -> Signal in switch result { - case let .botApp(botAppFlags, app): + case let .botApp(botAppFlags, app): switch app { case let .botApp(flags, id, accessHash, shortName, title, description, photo, document, hash): let _ = flags @@ -729,10 +763,13 @@ func _internal_getBotApp(account: Account, reference: BotAppReference) -> Signal if (botAppFlags & (1 << 1)) != 0 { appFlags.insert(.requiresWriteAccess) } + if (botAppFlags & (1 << 2)) != 0 { + appFlags.insert(.hasSettings) + } return .single(BotApp(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap(telegramMediaFileFromApiDocument), hash: hash, flags: appFlags)) - case .botAppNotModified: - return .complete() - } + case .botAppNotModified: + return .complete() + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 412728c0f2..fe302f6a7a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -528,6 +528,10 @@ public extension TelegramEngine { return _internal_removeBotFromAttachMenu(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, botId: botId) } + public func acceptAttachMenuBotDisclaimer(botId: PeerId) -> Signal { + return _internal_acceptAttachMenuBotDisclaimer(postbox: self.account.postbox, botId: botId) + } + public func getAttachMenuBot(botId: PeerId, cached: Bool = false) -> Signal { return _internal_getAttachMenuBot(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, botId: botId, cached: cached) } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD index 24e6e395b8..fa0c0f90a3 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD @@ -30,7 +30,6 @@ swift_library( "//submodules/UndoUI:UndoUI", "//submodules/ContextUI:ContextUI", "//submodules/GalleryUI:GalleryUI", - "//submodules/AttachmentTextInputPanelNode:AttachmentTextInputPanelNode", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramNotices:TelegramNotices", "//submodules/StickerPeekUI:StickerPeekUI", @@ -40,6 +39,7 @@ swift_library( "//submodules/FeaturedStickersScreen:FeaturedStickersScreen", "//submodules/StickerPackPreviewUI", "//submodules/TelegramUI/Components/EntityKeyboardGifContent:EntityKeyboardGifContent", + "//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView:LegacyMessageInputPanelInputView", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 6f36589a07..cc9e6658d5 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -32,6 +32,7 @@ import FeaturedStickersScreen import Pasteboard import StickerPackPreviewUI import EntityKeyboardGifContent +import LegacyMessageInputPanelInputView public final class EmptyInputView: UIView, UIInputViewAudioFeedback { public var enableInputClicksWhenVisible: Bool { @@ -2080,7 +2081,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } } -public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback { +public final class EntityInputView: UIInputView, LegacyMessageInputPanelInputView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback { private let context: AccountContext public var insertText: ((NSAttributedString) -> Void)? diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD b/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD index 9ffc71d6c7..ce295914a0 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD @@ -21,7 +21,9 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/ContextUI", "//submodules/TooltipUI", + "//submodules/UndoUI", "//submodules/TelegramUI/Components/MessageInputPanelComponent", + "//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index 86018480df..74e74c5c49 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -13,13 +13,17 @@ import MessageInputPanelComponent import TelegramPresentationData import ContextUI import TooltipUI +import LegacyMessageInputPanelInputView +import UndoUI public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { private let context: AccountContext private let chatLocation: ChatLocation + private let isScheduledMessages: Bool private let present: (ViewController) -> Void private let presentInGlobalOverlay: (ViewController) -> Void - + private let makeEntityInputView: () -> LegacyMessageInputPanelInputView? + private let state = ComponentState() private let inputPanelExternalState = MessageInputPanelComponent.ExternalState() private let inputPanel = ComponentView() @@ -27,21 +31,29 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { private var currentTimeout: Int32? private var currentIsEditing = false private var currentHeight: CGFloat? + private var currentIsVideo = false private let hapticFeedback = HapticFeedback() + private var inputView: LegacyMessageInputPanelInputView? + private var isEmojiKeyboardActive = false + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, metrics: LayoutMetrics)? public init( context: AccountContext, chatLocation: ChatLocation, + isScheduledMessages: Bool, present: @escaping (ViewController) -> Void, - presentInGlobalOverlay: @escaping (ViewController) -> Void + presentInGlobalOverlay: @escaping (ViewController) -> Void, + makeEntityInputView: @escaping () -> LegacyMessageInputPanelInputView? ) { self.context = context self.chatLocation = chatLocation + self.isScheduledMessages = isScheduledMessages self.present = present self.presentInGlobalOverlay = presentInGlobalOverlay + self.makeEntityInputView = makeEntityInputView super.init() @@ -84,23 +96,34 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { transition.setFrame(view: view, frame: frame) } - public func setTimeout(_ timeout: Int32) { + public func setTimeout(_ timeout: Int32, isVideo: Bool) { self.dismissTimeoutTooltip() var timeout: Int32? = timeout if timeout == 0 { timeout = nil } self.currentTimeout = timeout + self.currentIsVideo = isVideo } - public func dismissInput() { + public func dismissInput() -> Bool { if let view = self.inputPanel.view as? MessageInputPanelComponent.View { - view.deactivateInput() + if view.canDeactivateInput() { + self.isEmojiKeyboardActive = false + self.inputView = nil + view.deactivateInput(force: true) + return true + } else { + view.animateError() + return false + } + } else { + return true } } public func onAnimateOut() { - self.tooltipController?.dismiss() + self.dismissTimeoutTooltip() } public func baseHeight() -> CGFloat { @@ -145,6 +168,8 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { var maxInputPanelHeight = maxHeight if keyboardHeight.isZero { maxInputPanelHeight = 60.0 + } else { + maxInputPanelHeight = maxHeight - keyboardHeight - 100.0 } var resetInputContents: MessageInputPanelComponent.SendMessageInput? @@ -164,12 +189,16 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { strings: presentationData.strings, style: .media, placeholder: .plain(presentationData.strings.MediaPicker_AddCaption), - maxLength: 1024, + maxLength: Int(self.context.userLimits.maxCaptionLength), queryTypes: [.mention], alwaysDarkWhenHasText: false, resetInputContents: resetInputContents, - nextInputMode: { _ in - return .emoji + nextInputMode: { [weak self] _ in + if self?.isEmojiKeyboardActive == true { + return .text + } else { + return .emoji + } }, areVoiceMessagesAvailable: false, presentController: self.present, @@ -177,7 +206,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { sendMessageAction: { [weak self] in if let self { self.sendPressed?(self.caption()) - self.dismissInput() + let _ = self.dismissInput() } }, sendMessageOptionsAction: nil, @@ -190,8 +219,12 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { myReaction: nil, likeAction: nil, likeOptionsAction: nil, - inputModeAction: nil, - timeoutAction: self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser ? { [weak self] sourceView, gesture in + inputModeAction: { [weak self] in + if let self { + self.toggleInputMode() + } + }, + timeoutAction: self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser && !self.isScheduledMessages ? { [weak self] sourceView, gesture in if let self { self.presentTimeoutSetup(sourceView: sourceView, gesture: gesture) } @@ -214,6 +247,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { bottomInset: 0.0, isFormattingLocked: false, hideKeyboard: false, + customInputView: self.inputView, forceIsEditing: false, disabledPlaceholder: nil, isChannel: false, @@ -245,6 +279,47 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { return inputPanelSize.height - 8.0 } + private func toggleInputMode() { + self.isEmojiKeyboardActive = !self.isEmojiKeyboardActive + + if self.isEmojiKeyboardActive { + let inputView = self.makeEntityInputView() + inputView?.insertText = { [weak self] text in + if let self { + self.inputPanelExternalState.insertText(text) + } + } + inputView?.deleteBackwards = { [weak self] in + if let self { + self.inputPanelExternalState.deleteBackward() + } + } + inputView?.switchToKeyboard = { [weak self] in + if let self { + self.isEmojiKeyboardActive = false + self.inputView = nil + self.update(transition: .immediate) + } + } + inputView?.presentController = { [weak self] c in + if let self { + if !(c is UndoOverlayController) { + self.isEmojiKeyboardActive = false + if let view = self.inputPanel.view as? MessageInputPanelComponent.View { + view.deactivateInput(force: true) + } + } + self.present(c) + } + } + self.inputView = inputView + self.update(transition: .immediate) + } else { + self.inputView = nil + self.update(transition: .immediate) + } + } + private func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?) { self.hapticFeedback.impact(.light) @@ -252,10 +327,13 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { let updateTimeout: (Int32?) -> Void = { [weak self] timeout in if let self { + let previousTimeout = self.currentTimeout self.currentTimeout = timeout self.timerUpdated?(timeout as? NSNumber) self.update(transition: .immediate) - self.presentTimeoutTooltip(sourceView: sourceView, timeout: timeout) + if previousTimeout != timeout { + self.presentTimeoutTooltip(sourceView: sourceView, timeout: timeout) + } } } @@ -317,7 +395,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 2.0), size: CGSize()) - let isVideo = !"".isEmpty + let isVideo = self.currentIsVideo let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let text: String let iconName: String diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanelInputView/BUILD b/submodules/TelegramUI/Components/LegacyMessageInputPanelInputView/BUILD new file mode 100644 index 0000000000..e3708e9d4f --- /dev/null +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanelInputView/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "LegacyMessageInputPanelInputView", + module_name = "LegacyMessageInputPanelInputView", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanelInputView/Sources/LegacyMessageInputPanelInputView.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanelInputView/Sources/LegacyMessageInputPanelInputView.swift new file mode 100644 index 0000000000..a5e095dd9c --- /dev/null +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanelInputView/Sources/LegacyMessageInputPanelInputView.swift @@ -0,0 +1,11 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +public protocol LegacyMessageInputPanelInputView: UIView { + var insertText: ((NSAttributedString) -> Void)? { get set } + var deleteBackwards: (() -> Void)? { get set } + var switchToKeyboard: (() -> Void)? { get set } + var presentController: ((ViewController) -> Void)? { get set } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 33a08101c0..e01581104c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1214,6 +1214,7 @@ final class MediaEditorScreenComponent: Component { bottomInset: 0.0, isFormattingLocked: !state.isPremium, hideKeyboard: self.currentInputMode == .emoji, + customInputView: nil, forceIsEditing: self.currentInputMode == .emoji, disabledPlaceholder: nil, isChannel: false, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index 1cd86d4e2d..266eeee692 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -290,6 +290,7 @@ final class StoryPreviewComponent: Component { bottomInset: 0.0, isFormattingLocked: false, hideKeyboard: false, + customInputView: nil, forceIsEditing: false, disabledPlaceholder: nil, isChannel: false, diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index e561ebbf50..c5a260cec2 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -141,6 +141,7 @@ public final class MessageInputPanelComponent: Component { public let bottomInset: CGFloat public let isFormattingLocked: Bool public let hideKeyboard: Bool + public let customInputView: UIView? public let forceIsEditing: Bool public let disabledPlaceholder: String? public let isChannel: Bool @@ -193,6 +194,7 @@ public final class MessageInputPanelComponent: Component { bottomInset: CGFloat, isFormattingLocked: Bool, hideKeyboard: Bool, + customInputView: UIView?, forceIsEditing: Bool, disabledPlaceholder: String?, isChannel: Bool, @@ -244,6 +246,7 @@ public final class MessageInputPanelComponent: Component { self.bottomInset = bottomInset self.isFormattingLocked = isFormattingLocked self.hideKeyboard = hideKeyboard + self.customInputView = customInputView self.forceIsEditing = forceIsEditing self.disabledPlaceholder = disabledPlaceholder self.isChannel = isChannel @@ -327,6 +330,9 @@ public final class MessageInputPanelComponent: Component { if lhs.hideKeyboard != rhs.hideKeyboard { return false } + if lhs.customInputView !== rhs.customInputView { + return false + } if lhs.forceIsEditing != rhs.forceIsEditing { return false } @@ -712,6 +718,7 @@ public final class MessageInputPanelComponent: Component { textColor: UIColor(rgb: 0xffffff), insets: UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 48.0), hideKeyboard: component.hideKeyboard, + customInputView: component.customInputView, resetText: component.resetInputContents.flatMap { resetInputContents in switch resetInputContents { case let .text(value): diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index a2f19cae51..abb3d852da 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -2277,7 +2277,7 @@ final class StorageUsageScreenComponent: Component { } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(size, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string), elevatedLayout: false, action: { _ in return false }), in: .current) + controller.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(size, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } private func reloadStats(firstTime: Bool, completion: @escaping () -> Void) { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 424d4b3723..26282ee1d8 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2651,6 +2651,7 @@ public final class StoryItemSetContainerComponent: Component { bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset, isFormattingLocked: false, hideKeyboard: self.sendMessageContext.currentInputMode == .media, + customInputView: nil, forceIsEditing: self.sendMessageContext.currentInputMode == .media, disabledPlaceholder: disabledPlaceholder, isChannel: false, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index ef7ee915ba..d9f068eb40 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -1401,7 +1401,7 @@ final class StoryItemSetContainerSendMessage { peerType.insert(.sameBot) peerType.remove(.bot) } - let button: AttachmentButtonType = .app(bot.peer, bot.shortName, bot.icons) + let button: AttachmentButtonType = .app(bot) if !bot.peerTypes.intersection(peerType).isEmpty { buttons.insert(button, at: 1) @@ -1773,16 +1773,8 @@ final class StoryItemSetContainerSendMessage { }*/ //TODO:gift controller break - case let .app(bot, botName, _): - var payload: String? - var fromAttachMenu = true - /*if case let .bot(_, botPayload, _) = subject { - payload = botPayload - fromAttachMenu = false - }*/ - payload = nil - fromAttachMenu = true - let params = WebAppParameters(peerId: peer.id, botId: bot.id, botName: botName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: fromAttachMenu, isInline: false, isSimple: false) + case let .app(bot): + let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false) let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil) @@ -2290,7 +2282,7 @@ final class StoryItemSetContainerSendMessage { return nil } //TODO:self.presentationInterfaceState.customEmojiAvailable - return component.context.sharedContext.makeGalleryCaptionPanelView(context: component.context, chatLocation: .peer(id: peer.id), customEmojiAvailable: true, present: { [weak view] c in + return component.context.sharedContext.makeGalleryCaptionPanelView(context: component.context, chatLocation: .peer(id: peer.id), isScheduledMessages: false, customEmojiAvailable: true, present: { [weak view] c in guard let view else { return } diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index beb222bc18..ad7753684b 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -90,6 +90,7 @@ public final class TextFieldComponent: Component { public let textColor: UIColor public let insets: UIEdgeInsets public let hideKeyboard: Bool + public let customInputView: UIView? public let resetText: NSAttributedString? public let isOneLineWhenUnfocused: Bool public let formatMenuAvailability: FormatMenuAvailability @@ -105,6 +106,7 @@ public final class TextFieldComponent: Component { textColor: UIColor, insets: UIEdgeInsets, hideKeyboard: Bool, + customInputView: UIView?, resetText: NSAttributedString?, isOneLineWhenUnfocused: Bool, formatMenuAvailability: FormatMenuAvailability, @@ -119,6 +121,7 @@ public final class TextFieldComponent: Component { self.textColor = textColor self.insets = insets self.hideKeyboard = hideKeyboard + self.customInputView = customInputView self.resetText = resetText self.isOneLineWhenUnfocused = isOneLineWhenUnfocused self.formatMenuAvailability = formatMenuAvailability @@ -146,6 +149,9 @@ public final class TextFieldComponent: Component { if lhs.hideKeyboard != rhs.hideKeyboard { return false } + if lhs.customInputView !== rhs.customInputView { + return false + } if lhs.resetText != rhs.resetText { return false } @@ -816,9 +822,14 @@ public final class TextFieldComponent: Component { } while glyphIndexForStringStart < NSMaxRange(glyphRange) && !NSLocationInRange(glyphRange.location, lineRange) let padding = self.textView.textContainerInset.left - let rightmostX = lineRect.maxX + padding + var rightmostX = lineRect.maxX + padding let rightmostY = lineRect.minY + self.textView.textContainerInset.top + let stringIndex = self.textView.text.index(self.textView.text.startIndex, offsetBy: lineRange.location + lineRange.length - 1) + if self.textView.text[stringIndex] == " " { + rightmostX -= 3.0 + } + return CGPoint(x: rightmostX, y: rightmostY) } @@ -882,7 +893,14 @@ public final class TextFieldComponent: Component { component.externalState.isEditing = isEditing component.externalState.textLength = self.textStorage.string.count - if component.hideKeyboard { + if let inputView = component.customInputView { + if self.textView.inputView == nil { + self.textView.inputView = inputView + if self.textView.isFirstResponder { + self.textView.reloadInputViews() + } + } + } else if component.hideKeyboard { if self.textView.inputView == nil { self.textView.inputView = EmptyInputView() if self.textView.isFirstResponder { @@ -916,7 +934,7 @@ public final class TextFieldComponent: Component { view.alpha = 0.0 self.textView.addSubview(view) } - let ellipsisFrame = CGRect(origin: CGPoint(x: position.x - 11.0, y: position.y), size: ellipsisSize) + let ellipsisFrame = CGRect(origin: CGPoint(x: position.x - 8.0, y: position.y), size: ellipsisSize) transition.setFrame(view: view, frame: ellipsisFrame) let hasMoreThanOneLine = ellipsisFrame.maxY < self.textView.contentSize.height - 12.0 diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c16aa83df5..5acdcf0eaf 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4214,7 +4214,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return state.updatedShowWebView(true).updatedForceInputCommandsHidden(true) } - let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, fromMenu: true, fromAttachMenu: false, isInline: false, isSimple: false) + let params = WebAppParameters(source: .menu, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false) let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) }, requestSwitchInline: { [weak self] query, chatTypes, completion in @@ -4285,7 +4285,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let params = WebAppParameters(peerId: peerId, botId: botId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: false, isInline: isInline, isSimple: true) + let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peerId, botId: botId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false) let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) }, requestSwitchInline: { [weak self] query, chatTypes, completion in @@ -4325,7 +4325,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, fromMenu: false, fromAttachMenu: false, isInline: false, isSimple: false) + let params = WebAppParameters(source: .generic, peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false) let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) }, completion: { [weak self] in @@ -10318,7 +10318,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } unarchiveAutomaticallyArchivedPeer(account: strongSelf.context.account, peerId: peerId) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: strongSelf.presentationData.strings.Conversation_UnarchiveDone), elevatedLayout: false, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: strongSelf.presentationData.strings.Conversation_UnarchiveDone, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current) }, scrollToTop: { [weak self] in guard let strongSelf = self else { return @@ -12802,7 +12802,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G disposable.set((signal |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self, let _ = strongSelf.validLayout { - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string), elevatedLayout: false, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } })) @@ -12877,7 +12877,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func getCaptionPanelView() -> TGCaptionPanelView? { - return self.context.sharedContext.makeGalleryCaptionPanelView(context: self.context, chatLocation: self.presentationInterfaceState.chatLocation, customEmojiAvailable: self.presentationInterfaceState.customEmojiAvailable, present: { [weak self] c in + var isScheduledMessages = false + if case .scheduledMessages = self.presentationInterfaceState.subject { + isScheduledMessages = true + } + return self.context.sharedContext.makeGalleryCaptionPanelView(context: self.context, chatLocation: self.presentationInterfaceState.chatLocation, isScheduledMessages: isScheduledMessages, customEmojiAvailable: self.presentationInterfaceState.customEmojiAvailable, present: { [weak self] c in self?.present(c, in: .window(.root)) }, presentInGlobalOverlay: { [weak self] c in guard let self else { @@ -13046,7 +13050,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botApp.title, url: url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, fromMenu: false, fromAttachMenu: false, isInline: false, isSimple: false) + let params = WebAppParameters(source: .generic, peerId: peerId, botId: peerId, botName: botApp.title, url: url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings)) let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) }, requestSwitchInline: { [weak self] query, chatTypes, completion in @@ -13228,7 +13232,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G peerType.insert(.sameBot) peerType.remove(.bot) } - let button: AttachmentButtonType = .app(bot.peer, bot.shortName, bot.icons) + let button: AttachmentButtonType = .app(bot) if !bot.peerTypes.intersection(peerType).isEmpty { buttons.insert(button, at: 1) @@ -13269,21 +13273,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !premiumGiftOptions.isEmpty { buttons.insert(.gift, at: 1) } - + guard let initialButton = initialButton else { if case let .bot(botId, botPayload, botJustInstalled) = subject { if let button = allButtons.first(where: { button in - if case let .app(botPeer, _, _) = button, botPeer.id == botId { + if case let .app(bot) = button, bot.peer.id == botId { return true } else { return false } - }), case let .app(_, botName, _) = button { - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: botJustInstalled ? strongSelf.presentationData.strings.WebApp_AddToAttachmentSucceeded(botName).string : strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + }), case let .app(bot) = button { + let content: UndoOverlayContent + if botJustInstalled { + if bot.flags.contains(.showInSettings) { + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.shortName).string, timeout: 5.0) + } else { + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0) + } + } else { + content = .info(title: nil, text: strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError, timeout: nil) + } + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) } else { let _ = (context.engine.messages.getAttachMenuBot(botId: botId) |> deliverOnMainQueue).start(next: { bot in - let controller = addWebAppToAttachmentController(context: context, peerName: bot.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let controller = webAppTermsAlertController(context: context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { allowWrite in let _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite) |> deliverOnMainQueue).start(error: { _ in @@ -13308,7 +13322,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.canReadHistory.set(false) - let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: strongSelf.chatLocation, buttons: buttons, initialButton: initialButton, makeEntityInputView: { [weak self] in + let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: strongSelf.chatLocation, isScheduledMessages: isScheduledMessages, buttons: buttons, initialButton: initialButton, makeEntityInputView: { [weak self] in guard let strongSelf = self else { return nil } @@ -13435,7 +13449,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G contactsController.navigationPresentation = .modal completion(contactsController, contactsController.mediaPickerContext) strongSelf.controllerNavigationDisposable.set((contactsController.result - |> deliverOnMainQueue).start(next: { [weak self] peers in + |> deliverOnMainQueue).start(next: { [weak self] peers in if let strongSelf = self, let (peers, _, silent, scheduleTime, text) = peers { var textEnqueueMessage: EnqueueMessage? if let text = text, text.length > 0 { @@ -13607,14 +13621,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).start() } - case let .app(bot, botName, _): + case let .app(bot): var payload: String? var fromAttachMenu = true if case let .bot(_, botPayload, _) = subject { payload = botPayload fromAttachMenu = false } - let params = WebAppParameters(peerId: peer.id, botId: bot.id, botName: botName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: fromAttachMenu, isInline: false, isSimple: false) + let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false) let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageId, threadId: strongSelf.chatLocation.threadId) controller.openUrl = { [weak self] url, concealed, commit in @@ -13641,6 +13655,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G attachmentController.navigationPresentation = .flatModal strongSelf.push(attachmentController) strongSelf.attachmentController = attachmentController + + if case let .bot(botId, _, botJustInstalled) = subject, botJustInstalled { + if let button = allButtons.first(where: { button in + if case let .app(bot) = button, bot.peer.id == botId { + return true + } else { + return false + } + }), case let .app(bot) = button { + Queue.mainQueue().after(0.3) { + let content: UndoOverlayContent + if bot.flags.contains(.showInSettings) { + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.shortName).string, timeout: 5.0) + } else { + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0) + } + attachmentController.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) + } + } + } } if inputIsActive { @@ -14095,7 +14129,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } - let controller = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadTitle: self.threadInfo?.title, chatLocation: self.chatLocation, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos) + var isScheduledMessages = false + if case .scheduledMessages = self.presentationInterfaceState.subject { + isScheduledMessages = true + } + let controller = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadTitle: self.threadInfo?.title, chatLocation: self.chatLocation, isScheduledMessages: isScheduledMessages, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos) let mediaPickerContext = controller.mediaPickerContext controller.openCamera = { [weak self] cameraView in self?.openCamera(cameraView: cameraView) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 51fb13dc1b..a59385a5de 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1469,7 +1469,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuForward, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - interfaceInteraction.forwardMessages(selectAll ? messages : [message]) + interfaceInteraction.forwardMessages(selectAll || isImage ? messages : [message]) f(.dismissWithoutContent) }))) } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 28a40df703..9bac377525 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -4192,10 +4192,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode case let .message(message, _, _, _, _): for media in message.media { if let action = media as? TelegramMediaAction { - if case .phoneCall = action.action { } else { + if case .phoneCall = action.action { + } else { canHaveSelection = false break } + } else if media is TelegramMediaExpiredContent { + canHaveSelection = false } } if message.adAttribute != nil { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 336b5cb04e..13da105bb6 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -558,16 +558,10 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } case let .premiumOffer(reference): dismissInput() - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> deliverOnMainQueue).start(next: { peer in - let isPremium = peer?.isPremium ?? false - if !isPremium { - let controller = PremiumIntroScreen(context: context, source: .deeplink(reference)) - if let navigationController = navigationController { - navigationController.pushViewController(controller, animated: true) - } - } - }) + let controller = PremiumIntroScreen(context: context, source: .deeplink(reference)) + if let navigationController = navigationController { + navigationController.pushViewController(controller, animated: true) + } case let .joinVoiceChat(peerId, invite): let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in @@ -626,7 +620,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur return (chooseTypes?.isEmpty ?? true) ? nil : chooseTypes } - if let bot = attachMenuBots.first(where: { $0.peer.id == peerId }) { + if let bot = attachMenuBots.first(where: { $0.peer.id == peerId }), !bot.flags.contains(.notActivated) { let choose = filterChooseTypes(choose, peerTypes: bot.peerTypes) if let choose = choose { @@ -675,7 +669,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur |> deliverOnMainQueue).start(next: { bot in let choose = filterChooseTypes(choose, peerTypes: bot.peerTypes) - let controller = addWebAppToAttachmentController(context: context, peerName: bot.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let controller = webAppTermsAlertController(context: context, updatedPresentationData: updatedPresentationData, bot: bot, completion: { allowWrite in let _ = (context.engine.messages.addBotToAttachMenu(botId: peerId, allowWrite: allowWrite) |> deliverOnMainQueue).start(error: { _ in presentError(presentationData.strings.WebApp_AddToAttachmentUnavailableError) diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift index b42992ff22..0b4a4de9c4 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift @@ -1,5 +1,6 @@ import AsyncDisplayKit import Display +import SwiftSignalKit import TelegramPresentationData final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { @@ -31,13 +32,15 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { let label: Label let text: String let icon: UIImage? + let iconSignal: Signal? let action: (() -> Void)? - init(id: AnyHashable, label: Label = .none, text: String, icon: UIImage? = nil, action: (() -> Void)?) { + init(id: AnyHashable, label: Label = .none, text: String, icon: UIImage? = nil, iconSignal: Signal? = nil, action: (() -> Void)?) { self.id = id self.label = label self.text = text self.icon = icon + self.iconSignal = iconSignal self.action = action } @@ -57,6 +60,8 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { private let bottomSeparatorNode: ASDisplayNode private let activateArea: AccessibilityAreaNode + private var iconDisposable: Disposable? + private var item: PeerInfoScreenDisclosureItem? override init() { @@ -109,6 +114,10 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { self.addSubnode(self.activateArea) } + deinit { + self.iconDisposable?.dispose() + } + override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenDisclosureItem else { return 10.0 @@ -120,9 +129,9 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { self.selectionNode.pressed = item.action let sideInset: CGFloat = 16.0 + safeInsets.left - let leftInset = (item.icon == nil ? sideInset : sideInset + 29.0 + 16.0) + let leftInset = (item.icon == nil && item.iconSignal == nil ? sideInset : sideInset + 29.0 + 16.0) let rightInset = sideInset + 18.0 - let separatorInset = item.icon == nil ? sideInset : leftInset - 1.0 + let separatorInset = item.icon == nil && item.iconSignal == nil ? sideInset : leftInset - 1.0 let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor @@ -149,12 +158,29 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { let height = textSize.height + 24.0 - if let icon = item.icon { + if item.icon != nil || item.iconSignal != nil { if self.iconNode.supernode == nil { self.addSubnode(self.iconNode) } - self.iconNode.image = icon - let iconFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((height - icon.size.height) / 2.0)), size: icon.size) + let iconSize: CGSize + if let icon = item.icon { + self.iconNode.image = icon + iconSize = icon.size + } else if let iconSignal = item.iconSignal { + if previousItem?.text != item.text { + self.iconNode.image = nil + self.iconDisposable = (iconSignal + |> deliverOnMainQueue).start(next: { [weak self] icon in + if let self { + self.iconNode.image = icon + } + }) + } + iconSize = CGSize(width: 29.0, height: 29.0) + } else { + iconSize = CGSize(width: 29.0, height: 29.0) + } + let iconFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize) transition.updateFrame(node: self.iconNode, frame: iconFrame) } else if self.iconNode.supernode != nil { self.iconNode.image = nil diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index affa2df636..1fbd59c4cf 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -489,6 +489,25 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, } |> distinctUntilChanged + let botsKey = ValueBoxKey(length: 8) + botsKey.setInt64(0, value: 0) + let bots = context.engine.data.subscribe(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.attachMenuBots, id: botsKey)) + |> mapToSignal { entry -> Signal<[AttachMenuBot], NoError> in + let bots: [AttachMenuBots.Bot] = entry?.get(AttachMenuBots.self)?.bots ?? [] + return context.engine.data.subscribe( + EngineDataMap(bots.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.Peer.init)) + ) + |> map { peersMap -> [AttachMenuBot] in + var result: [AttachMenuBot] = [] + for bot in bots { + if let maybePeer = peersMap[bot.peerId], let peer = maybePeer { + result.append(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)) + } + } + return result + } + } + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), accountsAndPeers, @@ -512,7 +531,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, } |> distinctUntilChanged, hasStories, - context.engine.messages.attachMenuBots() + bots ) |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots -> PeerInfoScreenData in let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index ea7a57cc25..8b9d2a16ac 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -800,7 +800,20 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p if let settings = data.globalSettings { for bot in settings.bots { if bot.flags.contains(.showInSettings) { - items[.apps]!.append(PeerInfoScreenDisclosureItem(id: appIndex, text: bot.peer.compactDisplayTitle, icon: PresentationResourcesSettings.passport, action: { + let iconSignal: Signal + if let peer = PeerReference(bot.peer._asPeer()), let icon = bot.icons[.iOSSettingsStatic] { + let fileReference: FileMediaReference = .attachBot(peer: peer, media: icon) + iconSignal = instantPageImageFile(account: context.account, userLocation: .other, fileReference: fileReference, fetched: true) + |> map { generator -> UIImage? in + let size = CGSize(width: 29.0, height: 29.0) + let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: .zero)) + return context?.generateImage() + } + let _ = freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: fileReference).start() + } else { + iconSignal = .single(UIImage(bundleImageName: "Settings/Menu/Websites")!) + } + items[.apps]!.append(PeerInfoScreenDisclosureItem(id: bot.peer.id.id._internalGetInt64Value(), text: bot.peer.compactDisplayTitle, icon: nil, iconSignal: iconSignal, action: { interaction.openBotApp(bot) })) appIndex += 1 @@ -808,7 +821,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p } } - items[.apps]!.append(PeerInfoScreenDisclosureItem(id: appIndex, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: { + items[.apps]!.append(PeerInfoScreenDisclosureItem(id: 0, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: { interaction.openSettings(.stories) })) @@ -4070,6 +4083,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let previousSuggestPasswordConfirmation = previousData?.globalSettings?.suggestPasswordConfirmation, previousSuggestPasswordConfirmation != data.globalSettings?.suggestPasswordConfirmation { infoUpdated = true } + if let previousBots = previousData?.globalSettings?.bots, previousBots.count != (data.globalSettings?.bots ?? []).count { + infoUpdated = true + } } if previousCallsPrivate != currentCallsPrivate || (previousVideoCallsAvailable != currentVideoCallsAvailable && currentVideoCallsAvailable != nil) { infoUpdated = true @@ -4604,20 +4620,38 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let controller = self.controller else { return } - - let proceed = { [weak self] in + let presentationData = self.presentationData + let proceed: (Bool) -> Void = { [weak self] installed in guard let self else { return } - self.openBotAppDisposable.set(((self.context.engine.messages.requestSimpleWebView(botId: bot.peer.id, url: nil, source: .settings, themeParams: generateWebAppThemeParams(self.presentationData.theme)) + + let progressSignal = Signal { [weak self] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + self?.controller?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.35, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + let signal: Signal = self.context.engine.messages.requestSimpleWebView(botId: bot.peer.id, url: nil, source: .settings, themeParams: generateWebAppThemeParams(self.presentationData.theme)) |> afterDisposed { -// updateProgress() - }) + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + + self.openBotAppDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] url in guard let self else { return } - let params = WebAppParameters(peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, url: url, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: false, isInline: false, isSimple: true) + let params = WebAppParameters(source: .settings, peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, url: url, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: bot.flags.contains(.hasSettings)) let controller = standaloneWebAppController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { [weak self] url, concealed, commit in self?.openUrl(url: url, concealed: concealed, external: false, forceExternal: true, commit: commit) }, requestSwitchInline: { _, _, _ in @@ -4626,6 +4660,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) controller.navigationPresentation = .flatModal self.controller?.push(controller) + + if installed { + Queue.mainQueue().after(0.3, { + let text: String + if bot.flags.contains(.showInSettings) { + text = presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.peer.compactDisplayTitle).string + } else { + text = presentationData.strings.WebApp_ShortcutsAdded(bot.peer.compactDisplayTitle).string + } + controller.present( + UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: 5.0), elevatedLayout: false, position: .top, action: { _ in return false }), + in: .current + ) + }) + } }, error: { [weak self] error in if let self { self.controller?.present(textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { @@ -4634,13 +4683,28 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro })) } - if bot.flags.contains(.showInSettingsDisclaimer) { - let alertController = webAppTermsAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, peer: bot.peer, completion: { - proceed() + if bot.flags.contains(.notActivated) || bot.flags.contains(.showInSettingsDisclaimer) { + let alertController = webAppTermsAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, bot: bot, completion: { [weak self] allowWrite in + guard let self else { + return + } + if bot.flags.contains(.showInSettingsDisclaimer) { + let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).start() + } + if bot.flags.contains(.notActivated) { + let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite) + |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + proceed(true) + }) + } else { + proceed(false) + } }) controller.present(alertController, in: .window(.root)) } else { - proceed() + proceed(false) } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 0ea89466da..cf3431e753 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1624,109 +1624,17 @@ public final class SharedAccountContextImpl: SharedAccountContext { return makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, bannedSendMedia: bannedSendMedia, presentGallery: presentGallery, presentFiles: presentFiles, send: send) } - public func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? { -// var presentationData = context.sharedContext.currentPresentationData.with { $0 } -// presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) -// -// var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil) -// -// var updateChatPresentationInterfaceStateImpl: (((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void)? -// var ensureFocusedImpl: (() -> Void)? -// -// let interfaceInteraction = ChatPanelInterfaceInteraction(updateTextInputStateAndMode: { f in -// updateChatPresentationInterfaceStateImpl?({ -// let (updatedState, updatedMode) = f($0.interfaceState.effectiveInputState, $0.inputMode) -// return $0.updatedInterfaceState { interfaceState in -// return interfaceState.withUpdatedEffectiveInputState(updatedState) -// }.updatedInputMode({ _ in updatedMode }) -// }) -// }, updateInputModeAndDismissedButtonKeyboardMessageId: { f in -// updateChatPresentationInterfaceStateImpl?({ -// let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0) -// return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({ -// $0.withUpdatedMessageActionsState({ value in -// var value = value -// value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId -// return value -// }) -// }) -// }) -// }, openLinkEditing: { -// var selectionRange: Range? -// var text: NSAttributedString? -// var inputMode: ChatInputMode? -// updateChatPresentationInterfaceStateImpl?({ state in -// selectionRange = state.interfaceState.effectiveInputState.selectionRange -// if let selectionRange = selectionRange { -// text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count)) -// } -// inputMode = state.inputMode -// return state -// }) -// -// var link: String? -// if let text { -// text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in -// if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute { -// link = linkAttribute.url -// } -// } -// } -// -// let controller = chatTextLinkEditController(sharedContext: context.sharedContext, updatedPresentationData: (presentationData, .never()), account: context.account, text: text?.string ?? "", link: link, apply: { link in -// if let inputMode = inputMode, let selectionRange = selectionRange { -// if let link = link { -// updateChatPresentationInterfaceStateImpl?({ -// return $0.updatedInterfaceState({ -// $0.withUpdatedEffectiveInputState(chatTextInputAddLinkAttribute($0.effectiveInputState, selectionRange: selectionRange, url: link)) -// }) -// }) -// } -// ensureFocusedImpl?() -// updateChatPresentationInterfaceStateImpl?({ -// return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({ -// $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex)) -// }) -// }) -// } -// }) -// present(controller) -// }) -// -// let inputPanelNode = AttachmentTextInputPanelNode(context: context, presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { c in -// presentInGlobalOverlay(c) -// }, makeEntityInputView: { -// return EntityInputView(context: context, isDark: true, areCustomEmojiEnabled: customEmojiAvailable) -// }) -// inputPanelNode.interfaceInteraction = interfaceInteraction -// inputPanelNode.effectivePresentationInterfaceState = { -// return presentationInterfaceState -// } -// -// updateChatPresentationInterfaceStateImpl = { [weak inputPanelNode] f in -// let updatedPresentationInterfaceState = f(presentationInterfaceState) -// let updateInputTextState = presentationInterfaceState.interfaceState.effectiveInputState != updatedPresentationInterfaceState.interfaceState.effectiveInputState -// -// presentationInterfaceState = updatedPresentationInterfaceState -// -// if let inputPanelNode = inputPanelNode, updateInputTextState { -// inputPanelNode.updateInputTextState(updatedPresentationInterfaceState.interfaceState.effectiveInputState, animated: true) -// } -// } -// -// ensureFocusedImpl = { [weak inputPanelNode] in -// inputPanelNode?.ensureFocused() -// } -// -// return inputPanelNode - + public func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? { let inputPanelNode = LegacyMessageInputPanelNode( context: context, chatLocation: chatLocation, + isScheduledMessages: isScheduledMessages, present: present, - presentInGlobalOverlay: presentInGlobalOverlay + presentInGlobalOverlay: presentInGlobalOverlay, + makeEntityInputView: { + return EntityInputView(context: context, isDark: true, areCustomEmojiEnabled: customEmojiAvailable) + } ) - return inputPanelNode } diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 11f68ec1fb..5be099fc8e 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -11,7 +11,7 @@ public enum UndoOverlayContent { case archivedChat(peerId: Int64, title: String, text: String, undo: Bool) case hidArchive(title: String, text: String, undo: Bool) case revealedArchive(title: String, text: String, undo: Bool) - case succeed(text: String) + case succeed(text: String, timeout: Double?) case info(title: String?, text: String, timeout: Double?) case emoji(name: String, text: String) case swipeToReply(title: String, text: String) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index d003f623e1..e59855531f 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -191,7 +191,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { if text.contains("](") { isUserInteractionEnabled = true } - case let .succeed(text): + case let .succeed(text, timeout): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -204,7 +204,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.attributedText = attributedText self.textNode.maximumNumberOfLines = 5 displayUndo = false - self.originalRemainingSeconds = 3 + self.originalRemainingSeconds = timeout ?? 3 case let .info(title, text, timeout): self.avatarNode = nil self.iconNode = nil diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index ef88a3e302..878e6bcb76 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/InstantPageUI:InstantPageUI", "//submodules/CheckNode:CheckNode", "//submodules/Markdown:Markdown", + "//submodules/TextFormat:TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 7f07bf292d..11a57d9686 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -53,9 +53,10 @@ public class WebAppCancelButtonNode: ASDisplayNode { private let strings: PresentationStrings public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) { + let previousColor = self.color self.color = color - if case let .animated(duration, curve) = transition { + if case let .animated(duration, curve) = transition, previousColor != color { if let snapshotView = self.view.snapshotContentTree() { snapshotView.frame = self.bounds self.view.addSubview(snapshotView) @@ -170,6 +171,24 @@ public class WebAppCancelButtonNode: ASDisplayNode { } public struct WebAppParameters { + public enum Source { + case generic + case menu + case attachMenu + case inline + case simple + case settings + + var isSimple: Bool { + if [.simple, .inline, .settings].contains(self) { + return true + } else { + return false + } + } + } + + let source: Source let peerId: PeerId let botId: PeerId let botName: String @@ -178,12 +197,10 @@ public struct WebAppParameters { let payload: String? let buttonText: String? let keepAliveSignal: Signal? - let fromMenu: Bool - let fromAttachMenu: Bool - let isInline: Bool - let isSimple: Bool + let forceHasSettings: Bool public init( + source: Source, peerId: PeerId, botId: PeerId, botName: String, @@ -192,11 +209,9 @@ public struct WebAppParameters { payload: String?, buttonText: String?, keepAliveSignal: Signal?, - fromMenu: Bool, - fromAttachMenu: Bool, - isInline: Bool, - isSimple: Bool + forceHasSettings: Bool ) { + self.source = source self.peerId = peerId self.botId = botId self.botName = botName @@ -205,10 +220,7 @@ public struct WebAppParameters { self.payload = payload self.buttonText = buttonText self.keepAliveSignal = keepAliveSignal - self.fromMenu = fromMenu - self.fromAttachMenu = fromAttachMenu - self.isInline = isInline - self.isSimple = isSimple + self.forceHasSettings = forceHasSettings } } @@ -390,7 +402,7 @@ public final class WebAppController: ViewController, AttachmentContainable { }) }) - if let url = controller.url, !controller.fromMenu { + if let url = controller.url, controller.source != .menu { self.queryId = controller.queryId if let parsedUrl = URL(string: url) { self.webView?.load(URLRequest(url: parsedUrl)) @@ -409,7 +421,7 @@ public final class WebAppController: ViewController, AttachmentContainable { }) } } else { - let _ = (context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, payload: controller.payload, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: controller.fromMenu, replyToMessageId: controller.replyToMessageId, threadId: controller.threadId) + let _ = (context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, payload: controller.payload, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: controller.source == .menu, replyToMessageId: controller.replyToMessageId, threadId: controller.threadId) |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return @@ -704,11 +716,11 @@ public final class WebAppController: ViewController, AttachmentContainable { } } case "web_app_data_send": - if controller.isSimple, let eventData = body["eventData"] as? String { + if controller.source.isSimple, let eventData = body["eventData"] as? String { self.handleSendData(data: eventData) } case "web_app_setup_main_button": - if let webView = self.webView, !webView.didTouchOnce && controller.url == nil && controller.fromAttachMenu { + if let webView = self.webView, !webView.didTouchOnce && controller.url == nil && controller.source == .attachMenu { self.delayedScriptMessage = message } else if let json = json { if var isVisible = json["is_visible"] as? Bool { @@ -1298,6 +1310,7 @@ public final class WebAppController: ViewController, AttachmentContainable { fileprivate let moreButtonNode: MoreButtonNode private let context: AccountContext + private let source: WebAppParameters.Source private let peerId: PeerId private let botId: PeerId private let botName: String @@ -1305,10 +1318,7 @@ public final class WebAppController: ViewController, AttachmentContainable { private let queryId: Int64? private let payload: String? private let buttonText: String? - private let fromMenu: Bool - private let fromAttachMenu: Bool - private let isInline: Bool - private let isSimple: Bool + private let forceHasSettings: Bool private let keepAliveSignal: Signal? private let replyToMessageId: MessageId? private let threadId: Int64? @@ -1324,6 +1334,7 @@ public final class WebAppController: ViewController, AttachmentContainable { public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, params: WebAppParameters, replyToMessageId: MessageId?, threadId: Int64?) { self.context = context + self.source = params.source self.peerId = params.peerId self.botId = params.botId self.botName = params.botName @@ -1331,10 +1342,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.queryId = params.queryId self.payload = params.payload self.buttonText = params.buttonText - self.fromMenu = params.fromMenu - self.fromAttachMenu = params.fromAttachMenu - self.isInline = params.isInline - self.isSimple = params.isSimple + self.forceHasSettings = params.forceHasSettings self.keepAliveSignal = params.keepAliveSignal self.replyToMessageId = replyToMessageId self.threadId = threadId @@ -1446,15 +1454,30 @@ public final class WebAppController: ViewController, AttachmentContainable { let peerId = self.peerId let botId = self.botId + let url = self.url + let forceHasSettings = self.forceHasSettings + + let source = self.source let items = context.engine.messages.attachMenuBots() |> take(1) |> map { [weak self] attachMenuBots -> ContextController.Items in var items: [ContextMenuItem] = [] - let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId}) + let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId && !$0.flags.contains(.notActivated) }) - if self?.url == nil, let attachMenuBot = attachMenuBot, attachMenuBot.flags.contains(.hasSettings) { + let hasSettings: Bool + if url == nil { + if forceHasSettings { + hasSettings = true + } else { + hasSettings = attachMenuBot?.flags.contains(.hasSettings) == true + } + } else { + hasSettings = forceHasSettings + } + + if hasSettings { items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_Settings, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in @@ -1499,7 +1522,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self?.controllerNode.webView?.reload() }))) - if let _ = attachMenuBot, self?.url == nil { + if let _ = attachMenuBot, [.attachMenu, .settings].contains(source) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_RemoveBot, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in @@ -1654,7 +1677,7 @@ public func standaloneWebAppController( didDismiss: @escaping () -> Void = {}, getNavigationController: @escaping () -> NavigationController? = { return nil }, getSourceRect: (() -> CGRect?)? = nil) -> ViewController { - let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.fromMenu, hasTextInput: false, makeEntityInputView: { + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.source == .menu, hasTextInput: false, makeEntityInputView: { return nil }) controller.getInputContainerNode = getInputContainerNode diff --git a/submodules/WebUI/Sources/WebAppTermsAlertController.swift b/submodules/WebUI/Sources/WebAppTermsAlertController.swift index 81a4e2b6df..bbd5d81864 100644 --- a/submodules/WebUI/Sources/WebAppTermsAlertController.swift +++ b/submodules/WebUI/Sources/WebAppTermsAlertController.swift @@ -12,24 +12,27 @@ import AppBundle import AvatarNode import CheckNode import Markdown +import TextFormat private let textFont = Font.regular(13.0) private let boldTextFont = Font.semibold(13.0) -private func formattedText(_ text: String, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { - return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { _ in return nil}), textAlignment: textAlignment) +private func formattedText(_ text: String, fontSize: CGFloat, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { + return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: color), bold: MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: color), link: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: linkColor), linkAttribute: { _ in return (TelegramTextAttributes.URL, "") }), textAlignment: textAlignment) } -private final class WebAppTermsAlertContentNode: AlertContentNode { +private final class WebAppTermsAlertContentNode: AlertContentNode, UIGestureRecognizerDelegate { private let strings: PresentationStrings private let title: String private let text: String + private let additionalText: String? private let titleNode: ImmediateTextNode - private let textNode: ASTextNode + private let textNode: ImmediateTextNode + private let additionalTextNode: ImmediateTextNode private let acceptTermsCheckNode: InteractiveCheckNode - private let acceptTermsLabelNode: ASTextNode + private let acceptTermsLabelNode: ImmediateTextNode private let actionNodesSeparator: ASDisplayNode private let actionNodes: [TextAlertContentActionNode] @@ -50,24 +53,34 @@ private final class WebAppTermsAlertContentNode: AlertContentNode { } } - init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, actions: [TextAlertAction]) { + var openTerms: () -> Void = {} + + init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, additionalText: String?, actions: [TextAlertAction]) { self.strings = strings self.title = title self.text = text + self.additionalText = additionalText self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 1 self.titleNode.textAlignment = .center - self.textNode = ASTextNode() - self.textNode.displaysAsynchronously = false + self.textNode = ImmediateTextNode() self.textNode.maximumNumberOfLines = 0 + self.textNode.displaysAsynchronously = false + self.textNode.lineSpacing = 0.1 + self.textNode.textAlignment = .center + + self.additionalTextNode = ImmediateTextNode() + self.additionalTextNode.maximumNumberOfLines = 0 + self.additionalTextNode.displaysAsynchronously = false + self.additionalTextNode.lineSpacing = 0.1 + self.additionalTextNode.textAlignment = .center self.acceptTermsCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) - self.acceptTermsLabelNode = ASTextNode() + self.acceptTermsLabelNode = ImmediateTextNode() self.acceptTermsLabelNode.maximumNumberOfLines = 4 - self.acceptTermsLabelNode.isUserInteractionEnabled = true self.actionNodesSeparator = ASDisplayNode() self.actionNodesSeparator.isLayerBacked = true @@ -90,6 +103,7 @@ private final class WebAppTermsAlertContentNode: AlertContentNode { self.addSubnode(self.titleNode) self.addSubnode(self.textNode) + self.addSubnode(self.additionalTextNode) self.addSubnode(self.acceptTermsCheckNode) self.addSubnode(self.acceptTermsLabelNode) @@ -103,38 +117,98 @@ private final class WebAppTermsAlertContentNode: AlertContentNode { for separatorNode in self.actionVerticalSeparators { self.addSubnode(separatorNode) } - - if let firstAction = self.actionNodes.first { - firstAction.actionEnabled = false - } - + self.acceptTermsCheckNode.valueChanged = { [weak self] value in if let strongSelf = self { strongSelf.acceptedTerms = !strongSelf.acceptedTerms } } + self.acceptTermsLabelNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + } + self.acceptTermsLabelNode.tapAttributeAction = { [weak self] attributes, _ in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + self?.openTerms() + } + } + self.updateTheme(theme) } override func didLoad() { super.didLoad() - self.acceptTermsLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:)))) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:))) + tapGesture.delegate = self + self.view.addGestureRecognizer(tapGesture) + + if let firstAction = self.actionNodes.first { + firstAction.actionEnabled = false + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + let location = gestureRecognizer.location(in: self.acceptTermsLabelNode.view) + if self.acceptTermsLabelNode.bounds.contains(location) { + return true + } + return false + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + + if let (_, attributes) = self.acceptTermsLabelNode.attributesAtPoint(self.view.convert(point, to: self.acceptTermsLabelNode.view)) { + if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] == nil { + return self.view + } + } + + return super.hitTest(point, with: event) } @objc private func acceptTap(_ gestureRecognizer: UITapGestureRecognizer) { + let location = gestureRecognizer.location(in: self.acceptTermsLabelNode.view) if self.acceptTermsCheckNode.isUserInteractionEnabled { + if let attributes = self.acceptTermsLabelNode.attributesAtPoint(location)?.1 { + if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] != nil { + return + } + } self.acceptedTerms = !self.acceptedTerms } } override func updateTheme(_ theme: AlertControllerTheme) { self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = formattedText(self.text, fontSize: 13.0, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center) + if let additionalText = self.additionalText { + self.additionalTextNode.attributedText = formattedText(additionalText, fontSize: 13.0, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center) + } else { + self.additionalTextNode.attributedText = nil + } - let text = "I agree to the [Terms of Use]()" - self.acceptTermsLabelNode.attributedText = formattedText(text, color: theme.primaryColor, linkColor: theme.accentColor) + let attributedAgreeText = parseMarkdownIntoAttributedString( + self.strings.WebApp_DisclaimerAgree, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: theme.primaryColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.primaryColor), + link: MarkdownAttributeSet(font: textFont, textColor: theme.accentColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + ) + + self.acceptTermsLabelNode.attributedText = attributedAgreeText + self.acceptTermsLabelNode.linkHighlightColor = theme.accentColor.withAlphaComponent(0.2) self.actionNodesSeparator.backgroundColor = theme.separatorColor for actionNode in self.actionNodes { @@ -163,7 +237,7 @@ private final class WebAppTermsAlertContentNode: AlertContentNode { var entriesHeight: CGFloat = 0.0 - let textSize = self.textNode.measure(CGSize(width: size.width - 48.0, height: size.height)) + let textSize = self.textNode.updateLayout(CGSize(width: size.width - 48.0, height: size.height)) transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) origin.y += textSize.height @@ -175,7 +249,7 @@ private final class WebAppTermsAlertContentNode: AlertContentNode { let condensedSize = CGSize(width: size.width - 76.0, height: size.height) let spacing: CGFloat = 12.0 - let acceptTermsSize = self.acceptTermsLabelNode.measure(condensedSize) + let acceptTermsSize = self.acceptTermsLabelNode.updateLayout(condensedSize) let acceptTermsTotalWidth = checkSize.width + spacing + acceptTermsSize.width let acceptTermsOriginX = floorToScreenPixels((size.width - acceptTermsTotalWidth) / 2.0) @@ -183,6 +257,14 @@ private final class WebAppTermsAlertContentNode: AlertContentNode { transition.updateFrame(node: self.acceptTermsLabelNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX + checkSize.width + spacing, y: origin.y), size: acceptTermsSize)) origin.y += acceptTermsSize.height entriesHeight += acceptTermsSize.height + origin.y += 21.0 + } + + let additionalTextSize = self.additionalTextNode.updateLayout(CGSize(width: size.width - 48.0, height: size.height)) + transition.updateFrame(node: self.additionalTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - additionalTextSize.width) / 2.0), y: origin.y), size: additionalTextSize)) + origin.y += additionalTextSize.height + if additionalTextSize.height > 0.0 { + entriesHeight += 20.0 } let actionButtonHeight: CGFloat = 44.0 @@ -216,7 +298,7 @@ private final class WebAppTermsAlertContentNode: AlertContentNode { actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) } - let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 3.0 + insets.top + insets.bottom) + let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + additionalTextSize.height + entriesHeight + actionsHeight + 3.0 + insets.top + insets.bottom) transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) @@ -267,7 +349,12 @@ private final class WebAppTermsAlertContentNode: AlertContentNode { } } -public func webAppTermsAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, completion: @escaping () -> Void) -> AlertController { +public func webAppTermsAlertController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)?, + bot: AttachMenuBot, + completion: @escaping (Bool) -> Void +) -> AlertController { let theme = defaultDarkColorPresentationTheme let presentationData: PresentationData if let updatedPresentationData { @@ -278,18 +365,22 @@ public func webAppTermsAlertController(context: AccountContext, updatedPresentat let strings = presentationData.strings var dismissImpl: ((Bool) -> Void)? - let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "Continue", action: { - completion() + let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.WebApp_DisclaimerContinue, action: { + completion(true) dismissImpl?(true) }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { dismissImpl?(true) })] - let title = "Warning" - let text = "You are about to use a mini app operated by an independent party not affiliated with Telegram. You must agree to the Terms of Use of mini apps to continue." - - let contentNode = WebAppTermsAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, actions: actions) + let title = presentationData.strings.WebApp_DisclaimerTitle + let text = presentationData.strings.WebApp_DisclaimerText + let additionalText: String? = nil + let contentNode = WebAppTermsAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, additionalText: additionalText, actions: actions) + contentNode.openTerms = { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_Disclaimer_URL, forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: { + }) + } let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) dismissImpl = { [weak controller] animated in if animated {