mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
Emoji improvements
This commit is contained in:
parent
ee4cc8599c
commit
97de870643
@ -198,8 +198,8 @@ public struct ChatInterfaceForwardOptionsState: Codable, Equatable {
|
||||
}
|
||||
|
||||
public struct ChatTextInputState: Codable, Equatable {
|
||||
public let inputText: NSAttributedString
|
||||
public let selectionRange: Range<Int>
|
||||
public var inputText: NSAttributedString
|
||||
public var selectionRange: Range<Int>
|
||||
|
||||
public static func ==(lhs: ChatTextInputState, rhs: ChatTextInputState) -> Bool {
|
||||
return lhs.inputText.isEqual(to: rhs.inputText) && lhs.selectionRange == rhs.selectionRange
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<EntityKeyboardChildEnvironment, EntityKeyboardTopContainerPanelEnvironment>.View {
|
||||
return pagerView.centralId
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.pagerView = ComponentHostView<EntityKeyboardChildEnvironment>()
|
||||
|
||||
|
||||
@ -294,6 +294,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
private let loadingMessage = Promise<ChatLoadingMessageSubject?>(nil)
|
||||
private let performingInlineSearch = ValuePromise<Bool>(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
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<Int64>()
|
||||
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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user