mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Emoji improvements
This commit is contained in:
@@ -27,6 +27,8 @@ import EditableChatTextNode
|
||||
import EmojiTextAttachmentView
|
||||
import LottieAnimationComponent
|
||||
import ComponentFlow
|
||||
import EmojiSuggestionsComponent
|
||||
import AudioToolbox
|
||||
|
||||
private let accessoryButtonFont = Font.medium(14.0)
|
||||
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
|
||||
@@ -417,6 +419,47 @@ enum ChatTextInputPanelPasteData {
|
||||
case sticker(UIImage, Bool)
|
||||
}
|
||||
|
||||
final class ChatTextViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent {
|
||||
let ignoreHit: (UIView, CGPoint) -> Bool
|
||||
let dismissSuggestions: () -> Void
|
||||
|
||||
init(ignoreHit: @escaping (UIView, CGPoint) -> Bool, dismissSuggestions: @escaping () -> Void) {
|
||||
self.ignoreHit = ignoreHit
|
||||
self.dismissSuggestions = dismissSuggestions
|
||||
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func maybeDismissContent(point: CGPoint) {
|
||||
for subview in self.subviews.reversed() {
|
||||
if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.dismissSuggestions()
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
for subview in self.subviews.reversed() {
|
||||
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if event == nil || self.ignoreHit(self, point) {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.dismissSuggestions()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
final class CustomEmojiContainerView: UIView {
|
||||
private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView?
|
||||
|
||||
@@ -706,8 +749,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
|
||||
var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
||||
|
||||
private let presentationContext: ChatPresentationContext?
|
||||
|
||||
init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
|
||||
self.presentationInterfaceState = presentationInterfaceState
|
||||
self.presentationContext = presentationContext
|
||||
|
||||
var hasSpoilers = true
|
||||
if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
|
||||
@@ -770,6 +816,29 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
|
||||
super.init()
|
||||
|
||||
self.viewForOverlayContent = ChatTextViewForOverlayContent(
|
||||
ignoreHit: { [weak self] view, point in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
}
|
||||
if strongSelf.view.hitTest(view.convert(point, to: strongSelf.view), with: nil) != nil {
|
||||
return true
|
||||
}
|
||||
if view.convert(point, to: strongSelf.view).y > strongSelf.view.bounds.maxY {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
dismissSuggestions: { [weak self] in
|
||||
guard let strongSelf = self, let currentEmojiSuggestion = strongSelf.currentEmojiSuggestion, let textInputNode = strongSelf.textInputNode else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
|
||||
strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
|
||||
}
|
||||
)
|
||||
|
||||
self.context = context
|
||||
|
||||
self.addSubnode(self.clippingNode)
|
||||
@@ -1942,6 +2011,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom))
|
||||
let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size
|
||||
transition.updateFrame(node: textInputNode, frame: textFieldFrame)
|
||||
self.updateInputField(textInputFrame: textFieldFrame, transition: Transition(transition))
|
||||
if shouldUpdateLayout {
|
||||
textInputNode.layout()
|
||||
}
|
||||
@@ -2370,6 +2440,201 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private struct EmojiSuggestionPosition: Equatable {
|
||||
var range: NSRange
|
||||
var value: String
|
||||
}
|
||||
|
||||
private final class CurrentEmojiSuggestion {
|
||||
var localPosition: CGPoint
|
||||
var position: EmojiSuggestionPosition
|
||||
let disposable: MetaDisposable
|
||||
var value: [TelegramMediaFile]?
|
||||
|
||||
init(localPosition: CGPoint, position: EmojiSuggestionPosition, disposable: MetaDisposable, value: [TelegramMediaFile]?) {
|
||||
self.localPosition = localPosition
|
||||
self.position = position
|
||||
self.disposable = disposable
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
private var currentEmojiSuggestion: CurrentEmojiSuggestion?
|
||||
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
||||
|
||||
private var dismissedEmojiSuggestionPosition: EmojiSuggestionPosition?
|
||||
|
||||
private func updateInputField(textInputFrame: CGRect, transition: Transition) {
|
||||
guard let textInputNode = self.textInputNode, let context = self.context else {
|
||||
return
|
||||
}
|
||||
|
||||
var hasTracking = false
|
||||
var hasTrackingView = false
|
||||
if textInputNode.selectedRange.length == 0 && textInputNode.selectedRange.location > 0 {
|
||||
let selectedSubstring = textInputNode.textView.attributedText.attributedSubstring(from: NSRange(location: 0, length: textInputNode.selectedRange.location))
|
||||
if let lastCharacter = selectedSubstring.string.last, String(lastCharacter).isSingleEmoji {
|
||||
let queryLength = (String(lastCharacter) as NSString).length
|
||||
if selectedSubstring.attribute(ChatTextInputAttributes.customEmoji, at: selectedSubstring.length - queryLength, effectiveRange: nil) == nil {
|
||||
let beginning = textInputNode.textView.beginningOfDocument
|
||||
|
||||
let characterRange = NSRange(location: selectedSubstring.length - queryLength, length: queryLength)
|
||||
|
||||
let start = textInputNode.textView.position(from: beginning, offset: selectedSubstring.length - queryLength)
|
||||
let end = textInputNode.textView.position(from: beginning, offset: selectedSubstring.length)
|
||||
|
||||
if let start = start, let end = end, let textRange = textInputNode.textView.textRange(from: start, to: end) {
|
||||
let selectionRects = textInputNode.textView.selectionRects(for: textRange)
|
||||
let emojiSuggestionPosition = EmojiSuggestionPosition(range: characterRange, value: String(lastCharacter))
|
||||
|
||||
hasTracking = true
|
||||
|
||||
if let trackingRect = selectionRects.first?.rect {
|
||||
let trackingPosition = CGPoint(x: trackingRect.midX, y: trackingRect.minY)
|
||||
|
||||
if self.dismissedEmojiSuggestionPosition == emojiSuggestionPosition {
|
||||
} else {
|
||||
hasTrackingView = true
|
||||
|
||||
var beginRequest = false
|
||||
let suggestionContext: CurrentEmojiSuggestion
|
||||
if let current = self.currentEmojiSuggestion, current.position.value == emojiSuggestionPosition.value {
|
||||
suggestionContext = current
|
||||
} else {
|
||||
beginRequest = true
|
||||
suggestionContext = CurrentEmojiSuggestion(localPosition: trackingPosition, position: emojiSuggestionPosition, disposable: MetaDisposable(), value: nil)
|
||||
self.currentEmojiSuggestion = suggestionContext
|
||||
}
|
||||
suggestionContext.localPosition = trackingPosition
|
||||
self.dismissedEmojiSuggestionPosition = nil
|
||||
|
||||
if beginRequest {
|
||||
suggestionContext.disposable.set((EmojiSuggestionsComponent.suggestionData(context: context, query: String(lastCharacter))
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak suggestionContext] result in
|
||||
guard let strongSelf = self, let suggestionContext = suggestionContext, strongSelf.currentEmojiSuggestion === suggestionContext else {
|
||||
return
|
||||
}
|
||||
|
||||
suggestionContext.value = result
|
||||
|
||||
if let textInputNode = strongSelf.textInputNode {
|
||||
strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasTracking {
|
||||
self.dismissedEmojiSuggestionPosition = nil
|
||||
}
|
||||
|
||||
if let currentEmojiSuggestion = self.currentEmojiSuggestion, let value = currentEmojiSuggestion.value, value.isEmpty {
|
||||
hasTrackingView = false
|
||||
}
|
||||
if !textInputNode.textView.isFirstResponder {
|
||||
hasTrackingView = false
|
||||
}
|
||||
|
||||
if !hasTrackingView {
|
||||
if let currentEmojiSuggestion = self.currentEmojiSuggestion {
|
||||
self.currentEmojiSuggestion = nil
|
||||
currentEmojiSuggestion.disposable.dispose()
|
||||
}
|
||||
|
||||
if let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
|
||||
self.currentEmojiSuggestionView = nil
|
||||
|
||||
currentEmojiSuggestionView.alpha = 0.0
|
||||
currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, completion: { [weak currentEmojiSuggestionView] _ in
|
||||
currentEmojiSuggestionView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let context = self.context, let theme = self.theme, let viewForOverlayContent = self.viewForOverlayContent, let presentationContext = self.presentationContext, let currentEmojiSuggestion = self.currentEmojiSuggestion, let value = currentEmojiSuggestion.value {
|
||||
let currentEmojiSuggestionView: ComponentHostView<Empty>
|
||||
if let current = self.currentEmojiSuggestionView {
|
||||
currentEmojiSuggestionView = current
|
||||
} else {
|
||||
currentEmojiSuggestionView = ComponentHostView<Empty>()
|
||||
self.currentEmojiSuggestionView = currentEmojiSuggestionView
|
||||
viewForOverlayContent.addSubview(currentEmojiSuggestionView)
|
||||
|
||||
currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
|
||||
let globalPosition = textInputNode.textView.convert(currentEmojiSuggestion.localPosition, to: self.view)
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
let viewSize = currentEmojiSuggestionView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(EmojiSuggestionsComponent(
|
||||
context: context,
|
||||
theme: theme,
|
||||
animationCache: presentationContext.animationCache,
|
||||
animationRenderer: presentationContext.animationRenderer,
|
||||
files: value,
|
||||
action: { [weak self] file in
|
||||
guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction, let currentEmojiSuggestion = strongSelf.currentEmojiSuggestion else {
|
||||
return
|
||||
}
|
||||
|
||||
AudioServicesPlaySystemSound(0x450)
|
||||
|
||||
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
|
||||
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
|
||||
|
||||
var text: String?
|
||||
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
|
||||
loop: for attribute in file.attributes {
|
||||
switch attribute {
|
||||
case let .CustomEmoji(_, displayText, packReference):
|
||||
text = displayText
|
||||
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(stickerPack: packReference, fileId: file.fileId.id, file: file)
|
||||
break loop
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let emojiAttribute = emojiAttribute, let text = text {
|
||||
let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])
|
||||
|
||||
let range = currentEmojiSuggestion.position.range
|
||||
|
||||
inputText.replaceCharacters(in: range, with: replacementText)
|
||||
let selectionPosition = range.lowerBound + (replacementText.string as NSString).length
|
||||
|
||||
return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode)
|
||||
}
|
||||
|
||||
return (textInputState, inputMode)
|
||||
}
|
||||
|
||||
if let textInputNode = strongSelf.textInputNode {
|
||||
strongSelf.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
|
||||
strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: self.bounds.width - sideInset * 2.0, height: 100.0)
|
||||
)
|
||||
|
||||
let viewFrame = CGRect(origin: CGPoint(x: max(sideInset, floor(globalPosition.x - viewSize.width / 2.0)), y: globalPosition.y - 2.0 - viewSize.height), size: viewSize)
|
||||
currentEmojiSuggestionView.frame = viewFrame
|
||||
if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View {
|
||||
componentView.adjustBackground(relativePositionX: floor(globalPosition.x - viewFrame.minX))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCounterTextNode(transition: ContainedViewLayoutTransition) {
|
||||
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength {
|
||||
let textCount = Int32(textInputNode.textView.text.count)
|
||||
@@ -2580,6 +2845,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
|
||||
if !self.bounds.size.height.isEqual(to: panelHeight) {
|
||||
self.updateHeight(animated)
|
||||
} else {
|
||||
if let textInputNode = self.textInputNode {
|
||||
self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2644,6 +2913,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
|
||||
|
||||
self.updateSpoilersRevealed()
|
||||
|
||||
self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2671,6 +2942,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage
|
||||
self.inputMenu.deactivate()
|
||||
self.dismissedEmojiSuggestionPosition = nil
|
||||
|
||||
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||
if let peer = presentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil, presentationInterfaceState.keyboardButtonsMessage != nil {
|
||||
@@ -3128,6 +3400,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if self.bounds.contains(point), let textInputNode = self.textInputNode, let currentEmojiSuggestion = self.currentEmojiSuggestion, let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
|
||||
if let result = currentEmojiSuggestionView.hitTest(self.view.convert(point, to: currentEmojiSuggestionView), with: event) {
|
||||
return result
|
||||
}
|
||||
self.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
|
||||
self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
|
||||
}
|
||||
|
||||
let result = super.hitTest(point, with: event)
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user