From 97de8706435be1709dbe9d8561cd4ede2395f9f7 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 19 Jul 2022 13:18:22 +0200 Subject: [PATCH] Emoji improvements --- .../Sources/ChatController.swift | 4 +- .../Sources/EmojiPagerContentComponent.swift | 15 ++-- .../Sources/EntityKeyboard.swift | 8 ++ .../TelegramUI/Sources/ChatController.swift | 27 +++++++ .../Sources/ChatControllerNode.swift | 73 ++++++++++++++++--- .../Sources/ChatEntityKeyboardInputNode.swift | 9 +++ .../Sources/ChatInterfaceInputContexts.swift | 49 ++++++++++++- .../Sources/ChatTextInputPanelNode.swift | 31 +++++--- 8 files changed, 186 insertions(+), 30 deletions(-) diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index e0856e484f..ac9c4521d2 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -198,8 +198,8 @@ public struct ChatInterfaceForwardOptionsState: Codable, Equatable { } public struct ChatTextInputState: Codable, Equatable { - public let inputText: NSAttributedString - public let selectionRange: Range + public var inputText: NSAttributedString + public var selectionRange: Range public static func ==(lhs: ChatTextInputState, rhs: ChatTextInputState) -> Bool { return lhs.inputText.isEqual(to: rhs.inputText) && lhs.selectionRange == rhs.selectionRange diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 76871100e4..ee2b55af85 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -769,7 +769,7 @@ public final class EmojiPagerContentComponent: Component { context.scaleBy(x: 1.0 / scaleFactor, y: 1.0 / scaleFactor) - let string = NSAttributedString(string: staticEmoji, font: Font.regular(floor(30.0 * scaleFactor)), textColor: .black) + let string = NSAttributedString(string: staticEmoji, font: Font.regular(floor(32.0 * scaleFactor)), textColor: .black) let boundingRect = string.boundingRect(with: scaledSize, options: .usesLineFragmentOrigin, context: nil) UIGraphicsPushContext(context) string.draw(at: CGPoint(x: (scaledSize.width - boundingRect.width) / 2.0 + boundingRect.minX, y: (scaledSize.height - boundingRect.height) / 2.0 + boundingRect.minY)) @@ -969,6 +969,9 @@ public final class EmojiPagerContentComponent: Component { guard let item = strongSelf.item(atPoint: point), let itemLayer = strongSelf.visibleItemLayers[item.1], let file = item.0.file else { return nil } + if itemLayer.displayPlaceholder { + return nil + } let context = component.context let accountPeerId = context.account.peerId @@ -1059,11 +1062,11 @@ public final class EmojiPagerContentComponent: Component { loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, _, packReference): + case let .CustomEmoji(_, _, packReference), let .Sticker(_, packReference, _): if let packReference = packReference { let controller = context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: component.inputInteraction.navigationController(), sendSticker: { file, sourceView, sourceRect in - //return component.inputInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect, nil) - return false + component.inputInteraction.sendSticker?(file, false, false, nil, false, sourceView, sourceRect, nil) + return true }) component.inputInteraction.navigationController()?.view.window?.endEditing(true) @@ -1550,7 +1553,9 @@ public final class EmojiPagerContentComponent: Component { } if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self)), let itemLayer = self.visibleItemLayers[itemKey] { - component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer) + if !itemLayer.displayPlaceholder { + component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer) + } } } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index b86a6fdaad..6be760700b 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -176,6 +176,14 @@ public final class EntityKeyboardComponent: Component { private var topPanelExtension: CGFloat? private var isTopPanelExpanded: Bool = false + public var centralId: AnyHashable? { + if let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent.View { + return pagerView.centralId + } else { + return nil + } + } + override init(frame: CGRect) { self.pagerView = ComponentHostView() diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 9656d8f44d..66e2d188bc 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -294,6 +294,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private let loadingMessage = Promise(nil) private let performingInlineSearch = ValuePromise(false, ignoreRepeated: true) + private var stateServiceTasks: [AnyHashable: Disposable] = [:] + private var preloadHistoryPeerId: PeerId? private let preloadHistoryPeerIdDisposable = MetaDisposable() @@ -10079,6 +10081,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G completion(.immediate) } + let updatedServiceTasks = serviceTasksForChatPresentationIntefaceState(context: self.context, chatPresentationInterfaceState: updatedChatPresentationInterfaceState, updateState: { [weak self] f in + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, f) + + //strongSelf.chatDisplayNode.updateChatPresentationInterfaceState(f(strongSelf.chatDisplayNode.chatPresentationInterfaceState), transition: transition, interactive: false, completion: { _ in }) + }) + for (id, begin) in updatedServiceTasks { + if self.stateServiceTasks[id] == nil { + self.stateServiceTasks[id] = begin() + } + } + var removedServiceTaskIds: [AnyHashable] = [] + for (id, _) in self.stateServiceTasks { + if updatedServiceTasks[id] == nil { + removedServiceTaskIds.append(id) + } + } + for id in removedServiceTaskIds { + self.stateServiceTasks.removeValue(forKey: id)?.dispose() + } + if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, subject: self.subject, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { if self.leftNavigationButton != button { var animated = transition.isAnimated @@ -13008,6 +13033,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if self.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peerId, botId: results.botId, result: result, replyToMessageId: replyMessageId, hideVia: hideVia, silentPosting: silentPosting) { self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in var state = state state = state.updatedInterfaceState { interfaceState in diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index fefc2817bb..230b858fc1 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -19,6 +19,7 @@ import GridMessageSelectionNode import SparseItemGrid import ChatPresentationInterfaceState import ChatInputPanelContainer +import PremiumUI final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -602,6 +603,25 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.inputPanelContainerNode.toggleIfEnabled() } + self.textInputPanelNode?.switchToTextInputIfNeeded = { [weak self] in + guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction else { + return + } + + if let inputNode = strongSelf.inputNode as? ChatEntityKeyboardInputNode, !inputNode.canSwitchToTextInputAutomatically { + return + } + + interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + switch state.inputMode { + case .media: + return (.text, state.keyboardButtonsMessage?.id) + default: + return (state.inputMode, state.keyboardButtonsMessage?.id) + } + }) + } + self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() |> deliverOnMainQueue).start(next: { [weak self] value in guard let strongSelf = self else { @@ -2820,6 +2840,47 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { return } + var messages: [EnqueueMessage] = [] + + let effectiveInputText = effectivePresentationInterfaceState.interfaceState.composeInputState.inputText + + var inlineStickers: [MediaId: Media] = [:] + var firstLockedPremiumEmoji: TelegramMediaFile? + effectiveInputText.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: effectiveInputText.length), using: { value, _, _ in + if let value = value as? ChatTextInputTextCustomEmojiAttribute { + if let file = value.file { + inlineStickers[file.fileId] = file + if file.isPremiumEmoji && !self.chatPresentationInterfaceState.isPremium { + if firstLockedPremiumEmoji == nil { + firstLockedPremiumEmoji = file + } + } + } + } + }) + + if let firstLockedPremiumEmoji = firstLockedPremiumEmoji { + //let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + self.controllerInteraction.displayUndo(.sticker(context: context, file: firstLockedPremiumEmoji, title: nil, text: "Subscribe to Telegram Premium to unlock premium emoji.", undoText: "More", customAction: { [weak self] in + guard let strongSelf = self else { + return + } + + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumDemoScreen(context: strongSelf.context, subject: .premiumStickers, action: { + let controller = PremiumIntroScreen(context: strongSelf.context, source: .stickers) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + strongSelf.controller?.present(controller, in: .window(.root), with: nil) + })) + + return + } + let timestamp = CACurrentMediaTime() if self.lastSendTimestamp + 0.15 > timestamp { return @@ -2828,23 +2889,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.updateTypingActivity(false) - var messages: [EnqueueMessage] = [] - - let effectiveInputText = effectivePresentationInterfaceState.interfaceState.composeInputState.inputText let trimmedInputText = effectiveInputText.string.trimmingCharacters(in: .whitespacesAndNewlines) let peerId = effectivePresentationInterfaceState.chatLocation.peerId if peerId?.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = self.interactiveEmojis, interactiveEmojis.emojis.contains(trimmedInputText) { messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: trimmedInputText)), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil)) } else { - var inlineStickers: [MediaId: Media] = [:] - effectiveInputText.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: effectiveInputText.length), using: { value, _, _ in - if let value = value as? ChatTextInputTextCustomEmojiAttribute { - if let file = value.file { - inlineStickers[file.fileId] = file - } - } - }) - let inputText = convertMarkdownToAttributes(effectiveInputText) for text in breakChatInputText(trimChatInputText(inputText)) { diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index 4aa93627c8..f5dbe9d918 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -815,6 +815,15 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } + var canSwitchToTextInputAutomatically: Bool { + if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let centralId = pagerView.centralId { + if centralId == AnyHashable("emoji") { + return false + } + } + return true + } + private final class GifContext { private var componentValue: GifPagerContentComponent? { didSet { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 7c88fd6fb8..584cbc54fa 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -7,6 +7,8 @@ import AccountContext import Emoji import ChatInterfaceState import ChatPresentationInterfaceState +import SwiftSignalKit +import TextFormat struct PossibleContextQueryTypes: OptionSet { var rawValue: Int32 @@ -109,7 +111,9 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> let string = (inputString as String) let trimmedString = string.trimmingTrailingSpaces() if string.count < 3, trimmedString.isSingleEmoji { - return [(NSRange(location: 0, length: inputString.length - (string.count - trimmedString.count)), [.emoji], nil)] + if inputText.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) == nil { + return [(NSRange(location: 0, length: inputString.length - (string.count - trimmedString.count)), [.emoji], nil)] + } } var possibleTypes = PossibleContextQueryTypes([.command, .mention, .hashtag, .emojiSearch]) @@ -173,6 +177,49 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> return results } +func serviceTasksForChatPresentationIntefaceState(context: AccountContext, chatPresentationInterfaceState: ChatPresentationInterfaceState, updateState: @escaping ((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void) -> [AnyHashable: () -> Disposable] { + var missingEmoji = Set() + let inputText = chatPresentationInterfaceState.interfaceState.composeInputState.inputText + inputText.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: inputText.length), using: { value, _, _ in + if let value = value as? ChatTextInputTextCustomEmojiAttribute { + if value.file == nil { + missingEmoji.insert(value.fileId) + } + } + }) + + var result: [AnyHashable: () -> Disposable] = [:] + for id in missingEmoji { + result["emoji-\(id)"] = { + return (context.engine.stickers.resolveInlineStickers(fileIds: [id]) + |> deliverOnMainQueue).start(next: { result in + if let file = result[id] { + updateState({ state -> ChatPresentationInterfaceState in + return state.updatedInterfaceState { interfaceState -> ChatInterfaceState in + var inputState = interfaceState.composeInputState + let text = NSMutableAttributedString(attributedString: inputState.inputText) + + inputState.inputText.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: inputText.length), using: { value, range, _ in + if let value = value as? ChatTextInputTextCustomEmojiAttribute { + if value.fileId == id { + text.removeAttribute(ChatTextInputAttributes.customEmoji, range: range) + text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: file.fileId.id, file: file), range: range) + } + } + }) + + inputState.inputText = text + + return interfaceState.withUpdatedComposeInputState(inputState) + } + }) + } + }) + } + } + return result +} + func inputContextQueriesForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> [ChatPresentationInputQuery] { let inputState = chatPresentationInterfaceState.interfaceState.effectiveInputState let inputString: NSString = inputState.inputText.string as NSString diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 5298fa2532..28614e66cb 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -143,15 +143,15 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { static func calculateWidth(item: ChatTextInputAccessoryItem, image: UIImage?, text: String?, strings: PresentationStrings) -> CGFloat { switch item { - case .keyboard, .stickers, .inputButtons, .silentPost, .commands, .scheduledMessages: - return (image?.size.width ?? 0.0) + CGFloat(8.0) - case let .messageAutoremoveTimeout(timeout): - var imageWidth = (image?.size.width ?? 0.0) + CGFloat(8.0) - if let _ = timeout, let text = text { - imageWidth = ceil((text as NSString).size(withAttributes: [.font: accessoryButtonFont]).width) + 10.0 - } - - return max(imageWidth, 24.0) + case .keyboard, .stickers, .inputButtons, .silentPost, .commands, .scheduledMessages: + return 32.0 + case let .messageAutoremoveTimeout(timeout): + var imageWidth = (image?.size.width ?? 0.0) + CGFloat(8.0) + if let _ = timeout, let text = text { + imageWidth = ceil((text as NSString).size(withAttributes: [.font: accessoryButtonFont]).width) + 10.0 + } + + return max(imageWidth, 24.0) } } @@ -413,6 +413,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var paste: (ChatTextInputPanelPasteData) -> Void = { _ in } var updateHeight: (Bool) -> Void = { _ in } var toggleExpandMediaInput: (() -> Void)? + var switchToTextInputIfNeeded: (() -> Void)? var updateActivity: () -> Void = { } @@ -891,7 +892,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) recognizer.touchDown = { [weak self] in if let strongSelf = self { - strongSelf.ensureFocused() + strongSelf.ensureFocusedOnTap() } } textInputNode.view.addGestureRecognizer(recognizer) @@ -2904,6 +2905,16 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputNode?.becomeFirstResponder() } + func ensureFocusedOnTap() { + if self.textInputNode == nil { + self.loadTextInputNode() + } + + self.textInputNode?.becomeFirstResponder() + + self.switchToTextInputIfNeeded?() + } + func backwardsDeleteText() { guard let textInputNode = self.textInputNode else { return