mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
Emoji input and display in media selection
This commit is contained in:
parent
0577baac79
commit
eaf0b74f1b
@ -29,6 +29,11 @@ swift_library(
|
||||
"//submodules/Pasteboard:Pasteboard",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -19,6 +19,11 @@ import TextInputMenu
|
||||
import ChatPresentationInterfaceState
|
||||
import Pasteboard
|
||||
import EmojiTextAttachmentView
|
||||
import ComponentFlow
|
||||
import LottieAnimationComponent
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import TextNodeWithEntities
|
||||
|
||||
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
|
||||
private let minInputFontSize: CGFloat = 5.0
|
||||
@ -64,7 +69,7 @@ private func calculateTextFieldRealInsets(_ presentationInterfaceState: ChatPres
|
||||
top = 0.0
|
||||
bottom = 0.0
|
||||
}
|
||||
return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: 0.0)
|
||||
return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: 32.0)
|
||||
}
|
||||
|
||||
private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, UIImage)?
|
||||
@ -108,33 +113,6 @@ private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackground
|
||||
}
|
||||
}
|
||||
|
||||
private final class EntityInputView: UIInputView, UIInputViewAudioFeedback {
|
||||
override var inputViewStyle: UIInputView.Style {
|
||||
get {
|
||||
return .default
|
||||
}
|
||||
}
|
||||
override var allowsSelfSizing: Bool {
|
||||
get {
|
||||
return true
|
||||
}
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
CGSize(width: UIView.noIntrinsicMetric, height: 300)
|
||||
}
|
||||
|
||||
override func sizeToFit() {
|
||||
print("sizeToFit")
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return CGSize(width: size.width, height: 100.0)
|
||||
}
|
||||
}
|
||||
|
||||
private class CaptionEditableTextNode: EditableTextNode {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let previousAlpha = self.alpha
|
||||
@ -145,18 +123,91 @@ private class CaptionEditableTextNode: EditableTextNode {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol AttachmentTextInputPanelInputView: UIView {
|
||||
var insertText: ((NSAttributedString) -> Void)? { get set }
|
||||
var deleteBackwards: (() -> Void)? { get set }
|
||||
var switchToKeyboard: (() -> Void)? { get set }
|
||||
var presentController: ((ViewController) -> Void)? { get set }
|
||||
}
|
||||
|
||||
final class CustomEmojiContainerView: UIView {
|
||||
private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView?
|
||||
|
||||
private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:]
|
||||
|
||||
init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) {
|
||||
self.emojiViewProvider = emojiViewProvider
|
||||
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) {
|
||||
var nextIndexById: [Int64: Int] = [:]
|
||||
|
||||
var validKeys = Set<InlineStickerItemLayer.Key>()
|
||||
for (rect, emoji) in emojiRects {
|
||||
let index: Int
|
||||
if let nextIndex = nextIndexById[emoji.fileId] {
|
||||
index = nextIndex
|
||||
} else {
|
||||
index = 0
|
||||
}
|
||||
nextIndexById[emoji.fileId] = index + 1
|
||||
|
||||
let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index)
|
||||
|
||||
let view: UIView
|
||||
if let current = self.emojiLayers[key] {
|
||||
view = current
|
||||
} else if let newView = self.emojiViewProvider(emoji) {
|
||||
view = newView
|
||||
self.addSubview(newView)
|
||||
self.emojiLayers[key] = view
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
let size = CGSize(width: 24.0, height: 24.0)
|
||||
|
||||
view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0)), size: size)
|
||||
|
||||
validKeys.insert(key)
|
||||
}
|
||||
|
||||
var removeKeys: [InlineStickerItemLayer.Key] = []
|
||||
for (key, view) in self.emojiLayers {
|
||||
if !validKeys.contains(key) {
|
||||
removeKeys.append(key)
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
for key in removeKeys {
|
||||
self.emojiLayers.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, ASEditableTextNodeDelegate {
|
||||
private let context: AccountContext
|
||||
|
||||
private let isCaption: Bool
|
||||
private let isAttachment: Bool
|
||||
|
||||
private let presentController: (ViewController) -> Void
|
||||
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
|
||||
|
||||
private var textPlaceholderNode: ImmediateTextNode
|
||||
private let textInputContainerBackgroundNode: ASImageNode
|
||||
private let textInputContainer: ASDisplayNode
|
||||
public var textInputNode: EditableTextNode?
|
||||
private var dustNode: InvisibleInkDustNode?
|
||||
private var oneLineNode: ImmediateTextNode
|
||||
private var customEmojiContainerView: CustomEmojiContainerView?
|
||||
private var oneLineNode: TextNodeWithEntities
|
||||
private var oneLineNodeAttributedText: NSAttributedString?
|
||||
private var oneLineDustNode: InvisibleInkDustNode?
|
||||
|
||||
let textInputBackgroundNode: ASDisplayNode
|
||||
@ -164,6 +215,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
private var transparentTextInputBackgroundImage: UIImage?
|
||||
private let actionButtons: AttachmentTextInputActionButtonsNode
|
||||
private let counterTextNode: ImmediateTextNode
|
||||
|
||||
private let inputModeView: ComponentHostView<Empty>
|
||||
|
||||
private var validLayout: (CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool)?
|
||||
|
||||
@ -270,14 +323,23 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
private var spoilersRevealed = false
|
||||
|
||||
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
||||
private let animationCache: AnimationCache
|
||||
private let animationRenderer: MultiAnimationRenderer
|
||||
|
||||
private var maxCaptionLength: Int32?
|
||||
|
||||
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, presentController: @escaping (ViewController) -> Void) {
|
||||
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, presentController: @escaping (ViewController) -> Void, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
|
||||
self.context = context
|
||||
self.presentationInterfaceState = presentationInterfaceState
|
||||
self.isCaption = isCaption
|
||||
self.isAttachment = isAttachment
|
||||
self.presentController = presentController
|
||||
self.makeEntityInputView = makeEntityInputView
|
||||
|
||||
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
|
||||
return TempBox.shared.tempFile(fileName: "file").path
|
||||
})
|
||||
self.animationRenderer = MultiAnimationRendererImpl()
|
||||
|
||||
var hasSpoilers = true
|
||||
if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
|
||||
@ -293,6 +355,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
if !isCaption {
|
||||
self.textInputContainer.addSubnode(self.textInputContainerBackgroundNode)
|
||||
}
|
||||
|
||||
self.inputModeView = ComponentHostView<Empty>()
|
||||
self.textInputContainer.view.addSubview(self.inputModeView)
|
||||
self.textInputContainer.clipsToBounds = true
|
||||
|
||||
self.textInputBackgroundNode = ASDisplayNode()
|
||||
@ -303,9 +368,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
self.textPlaceholderNode.maximumNumberOfLines = 1
|
||||
self.textPlaceholderNode.isUserInteractionEnabled = false
|
||||
|
||||
self.oneLineNode = ImmediateTextNode()
|
||||
self.oneLineNode.maximumNumberOfLines = 1
|
||||
self.oneLineNode.isUserInteractionEnabled = false
|
||||
self.oneLineNode = TextNodeWithEntities()
|
||||
self.oneLineNode.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.actionButtons = AttachmentTextInputActionButtonsNode(presentationInterfaceState: presentationInterfaceState, presentController: presentController)
|
||||
self.counterTextNode = ImmediateTextNode()
|
||||
@ -331,7 +395,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
self.addSubnode(self.counterTextNode)
|
||||
|
||||
if isCaption {
|
||||
self.addSubnode(self.oneLineNode)
|
||||
self.addSubnode(self.oneLineNode.textNode)
|
||||
}
|
||||
|
||||
self.textInputBackgroundImageNode.clipsToBounds = true
|
||||
@ -343,13 +407,13 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
}
|
||||
self.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
|
||||
|
||||
/*self.emojiViewProvider = { [weak self] emoji in
|
||||
guard let strongSelf = self, let file = strongSelf.context.animatedEmojiStickers[emoji]?.first?.file else {
|
||||
self.emojiViewProvider = { [weak self] emoji in
|
||||
guard let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState else {
|
||||
return UIView()
|
||||
}
|
||||
|
||||
return EmojiTextAttachmentView(context: context, file: file)
|
||||
}*/
|
||||
return EmojiTextAttachmentView(context: context, emoji: emoji, file: emoji.file, cache: strongSelf.animationCache, renderer: strongSelf.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12))
|
||||
}
|
||||
|
||||
self.updateSendButtonEnabled(isCaption || isAttachment, animated: false)
|
||||
|
||||
@ -464,11 +528,6 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
textInputNode.view.addGestureRecognizer(recognizer)
|
||||
|
||||
textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string
|
||||
|
||||
/*let entityInputView = EntityInputView()
|
||||
entityInputView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 100.0))
|
||||
entityInputView.backgroundColor = .blue
|
||||
textInputNode.textView.inputView = entityInputView*/
|
||||
}
|
||||
|
||||
private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat {
|
||||
@ -528,6 +587,14 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
return minimalHeight
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.inputModeView.isHidden, let result = self.inputModeView.hitTest(self.view.convert(point, to: self.inputModeView), with: event) {
|
||||
return result
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
|
||||
let hadLayout = self.validLayout != nil
|
||||
let previousAdditionalSideInsets = self.validLayout?.3
|
||||
@ -688,7 +755,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
if self.isCaption {
|
||||
if self.isFocused {
|
||||
self.oneLineNode.alpha = 0.0
|
||||
self.oneLineNode.textNode.alpha = 0.0
|
||||
self.oneLineDustNode?.alpha = 0.0
|
||||
self.textInputNode?.alpha = 1.0
|
||||
|
||||
@ -698,7 +765,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
} else {
|
||||
panelHeight = minimalHeight
|
||||
|
||||
transition.updateAlpha(node: self.oneLineNode, alpha: inputHasText ? 1.0 : 0.0)
|
||||
transition.updateAlpha(node: self.oneLineNode.textNode, alpha: inputHasText ? 1.0 : 0.0)
|
||||
if let oneLineDustNode = self.oneLineDustNode {
|
||||
transition.updateAlpha(node: oneLineDustNode, alpha: inputHasText ? 1.0 : 0.0)
|
||||
}
|
||||
@ -711,9 +778,34 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
transition.updateAlpha(node: self.textInputBackgroundImageNode, alpha: inputHasText ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
let oneLineSize = self.oneLineNode.updateLayout(CGSize(width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: CGFloat.greatestFiniteMagnitude))
|
||||
let oneLineFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: oneLineSize)
|
||||
self.oneLineNode.frame = oneLineFrame
|
||||
let makeOneLineLayout = TextNodeWithEntities.asyncLayout(self.oneLineNode)
|
||||
let (oneLineLayout, oneLineApply) = makeOneLineLayout(TextNodeLayoutArguments(
|
||||
attributedString: self.oneLineNodeAttributedText,
|
||||
backgroundColor: nil,
|
||||
minimumNumberOfLines: 1,
|
||||
maximumNumberOfLines: 1,
|
||||
truncationType: .end,
|
||||
constrainedSize: CGSize(width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: CGFloat.greatestFiniteMagnitude),
|
||||
alignment: .left,
|
||||
verticalAlignment: .top,
|
||||
lineSpacing: 0.0,
|
||||
cutout: nil, insets: UIEdgeInsets(),
|
||||
lineColor: nil,
|
||||
textShadowColor: nil,
|
||||
textStroke: nil,
|
||||
displaySpoilers: false,
|
||||
displayEmbeddedItemsUnderSpoilers: false
|
||||
))
|
||||
|
||||
let oneLineFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: oneLineLayout.size)
|
||||
self.oneLineNode.textNode.frame = oneLineFrame
|
||||
let _ = oneLineApply(TextNodeWithEntities.Arguments(
|
||||
context: self.context,
|
||||
cache: self.animationCache,
|
||||
renderer: self.animationRenderer,
|
||||
placeholderColor: self.presentationInterfaceState?.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12) ?? .lightGray,
|
||||
attemptSynchronous: false
|
||||
))
|
||||
|
||||
self.updateOneLineSpoiler()
|
||||
}
|
||||
@ -725,6 +817,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
if let textInputNode = self.textInputNode {
|
||||
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
|
||||
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||
textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState)
|
||||
}
|
||||
transition.updateFrame(node: textInputNode, frame: textFieldFrame)
|
||||
if shouldUpdateLayout {
|
||||
textInputNode.layout()
|
||||
@ -788,6 +883,37 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
var textInputViewRealInsets = UIEdgeInsets()
|
||||
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState)
|
||||
|
||||
var colors: [String: UIColor] = [:]
|
||||
let colorKeys: [String] = [
|
||||
"Ellipse 33.Ellipse 33.Stroke 1",
|
||||
"Ellipse 34.Ellipse 34.Stroke 1",
|
||||
"Oval.Oval.Fill 1",
|
||||
"Oval 2.Oval.Fill 1",
|
||||
"Path 85.Path 85.Stroke 1"
|
||||
]
|
||||
for colorKey in colorKeys {
|
||||
colors[colorKey] = presentationInterfaceState.theme.chat.inputPanel.inputControlColor
|
||||
}
|
||||
let animationComponent = LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: "anim_smiletosticker",
|
||||
colors: colors,
|
||||
mode: .animateTransitionFromPrevious
|
||||
),
|
||||
size: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
let inputNodeSize = self.inputModeView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(animationComponent),
|
||||
action: { [weak self] in
|
||||
self?.toggleInputMode()
|
||||
})),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
transition.updateFrame(view: self.inputModeView, frame: CGRect(origin: CGPoint(x: textInputBackgroundFrame.maxX - inputNodeSize.width - 1.0, y: textInputBackgroundFrame.maxY - inputNodeSize.height - 1.0), size: inputNodeSize))
|
||||
}
|
||||
|
||||
let placeholderFrame: CGRect
|
||||
@ -836,6 +962,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
||||
|
||||
var rects: [CGRect] = []
|
||||
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
|
||||
|
||||
if let attributedText = textInputNode.attributedText {
|
||||
let beginning = textInputNode.textView.beginningOfDocument
|
||||
@ -873,6 +1000,16 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
|
||||
}
|
||||
}
|
||||
|
||||
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||
if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) {
|
||||
let textRects = textInputNode.textView.selectionRects(for: textRange)
|
||||
for textRect in textRects {
|
||||
customEmojiRects.append((textRect.rect, value))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -893,6 +1030,28 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
dustNode.removeFromSupernode()
|
||||
self.dustNode = nil
|
||||
}
|
||||
|
||||
if !customEmojiRects.isEmpty {
|
||||
let customEmojiContainerView: CustomEmojiContainerView
|
||||
if let current = self.customEmojiContainerView {
|
||||
customEmojiContainerView = current
|
||||
} else {
|
||||
customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
|
||||
guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else {
|
||||
return nil
|
||||
}
|
||||
return emojiViewProvider(emoji)
|
||||
})
|
||||
customEmojiContainerView.isUserInteractionEnabled = false
|
||||
textInputNode.textView.addSubview(customEmojiContainerView)
|
||||
self.customEmojiContainerView = customEmojiContainerView
|
||||
}
|
||||
|
||||
customEmojiContainerView.update(emojiRects: customEmojiRects)
|
||||
} else if let customEmojiContainerView = self.customEmojiContainerView {
|
||||
customEmojiContainerView.removeFromSuperview()
|
||||
self.customEmojiContainerView = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSpoilersRevealed(animated: Bool = true) {
|
||||
@ -1016,6 +1175,71 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleInputMode() {
|
||||
self.loadTextInputNodeIfNeeded()
|
||||
|
||||
guard let textInputNode = self.textInputNode else {
|
||||
return
|
||||
}
|
||||
|
||||
var shouldHaveInputView = false
|
||||
if textInputNode.textView.isFirstResponder {
|
||||
if textInputNode.textView.inputView == nil {
|
||||
shouldHaveInputView = true
|
||||
}
|
||||
} else {
|
||||
shouldHaveInputView = true
|
||||
}
|
||||
|
||||
if shouldHaveInputView {
|
||||
let inputView = self.makeEntityInputView()
|
||||
inputView?.insertText = { [weak self] text in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { textInputState, inputMode in
|
||||
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
|
||||
|
||||
let range = textInputState.selectionRange
|
||||
inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: text)
|
||||
|
||||
let selectionPosition = range.lowerBound + (text.string as NSString).length
|
||||
|
||||
return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode)
|
||||
}
|
||||
}
|
||||
inputView?.deleteBackwards = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.textInputNode?.textView.deleteBackward()
|
||||
}
|
||||
inputView?.switchToKeyboard = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.toggleInputMode()
|
||||
}
|
||||
inputView?.presentController = { [weak self] c in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.presentController(c)
|
||||
}
|
||||
|
||||
textInputNode.textView.inputView = inputView
|
||||
} else {
|
||||
textInputNode.textView.inputView = nil
|
||||
}
|
||||
|
||||
if textInputNode.textView.isFirstResponder {
|
||||
textInputNode.textView.reloadInputViews()
|
||||
} else {
|
||||
textInputNode.textView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTextNodeText(animated: Bool) {
|
||||
var inputHasText = false
|
||||
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
|
||||
@ -1037,12 +1261,12 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
let trimmedText = NSMutableAttributedString(attributedString: attributedText.attributedSubstring(from: NSMakeRange(0, range.location)))
|
||||
trimmedText.append(NSAttributedString(string: "\u{2026}", font: textFont, textColor: textColor))
|
||||
|
||||
self.oneLineNode.attributedText = trimmedText
|
||||
self.oneLineNodeAttributedText = trimmedText
|
||||
} else {
|
||||
self.oneLineNode.attributedText = attributedText
|
||||
self.oneLineNodeAttributedText = attributedText
|
||||
}
|
||||
} else {
|
||||
self.oneLineNode.attributedText = nil
|
||||
self.oneLineNodeAttributedText = nil
|
||||
}
|
||||
|
||||
let panelHeight = self.updateTextHeight(animated: animated)
|
||||
@ -1052,15 +1276,15 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
}
|
||||
|
||||
private func updateOneLineSpoiler() {
|
||||
if let textLayout = self.oneLineNode.cachedLayout, !textLayout.spoilers.isEmpty {
|
||||
if let textLayout = self.oneLineNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty {
|
||||
if self.oneLineDustNode == nil {
|
||||
let oneLineDustNode = InvisibleInkDustNode(textNode: nil)
|
||||
self.oneLineDustNode = oneLineDustNode
|
||||
self.oneLineNode.supernode?.insertSubnode(oneLineDustNode, aboveSubnode: self.oneLineNode)
|
||||
self.oneLineNode.textNode.supernode?.insertSubnode(oneLineDustNode, aboveSubnode: self.oneLineNode.textNode)
|
||||
|
||||
}
|
||||
if let oneLineDustNode = self.oneLineDustNode {
|
||||
let textFrame = self.oneLineNode.frame.insetBy(dx: 0.0, dy: -3.0)
|
||||
let textFrame = self.oneLineNode.textNode.frame.insetBy(dx: 0.0, dy: -3.0)
|
||||
|
||||
oneLineDustNode.update(size: textFrame.size, color: .white, textColor: .white, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 0.0, dy: 3.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 0.0, dy: 3.0) })
|
||||
oneLineDustNode.frame = textFrame
|
||||
|
||||
@ -11,6 +11,7 @@ import AccountContext
|
||||
import TelegramStringFormatting
|
||||
import UIKitRuntimeUtils
|
||||
import MediaResources
|
||||
import AttachmentTextInputPanelNode
|
||||
|
||||
public enum AttachmentButtonType: Equatable {
|
||||
case gallery
|
||||
@ -167,6 +168,7 @@ public class AttachmentController: ViewController {
|
||||
private let buttons: [AttachmentButtonType]
|
||||
private let initialButton: AttachmentButtonType
|
||||
private let fromMenu: Bool
|
||||
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
|
||||
|
||||
public var willDismiss: () -> Void = {}
|
||||
public var didDismiss: () -> Void = {}
|
||||
@ -190,6 +192,7 @@ public class AttachmentController: ViewController {
|
||||
private let dim: ASDisplayNode
|
||||
private let shadowNode: ASImageNode
|
||||
private let container: AttachmentContainer
|
||||
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
|
||||
let panel: AttachmentPanel
|
||||
|
||||
private var currentType: AttachmentButtonType?
|
||||
@ -259,8 +262,9 @@ public class AttachmentController: ViewController {
|
||||
|
||||
private let wrapperNode: ASDisplayNode
|
||||
|
||||
init(controller: AttachmentController) {
|
||||
init(controller: AttachmentController, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
|
||||
self.controller = controller
|
||||
self.makeEntityInputView = makeEntityInputView
|
||||
|
||||
self.dim = ASDisplayNode()
|
||||
self.dim.alpha = 0.0
|
||||
@ -274,7 +278,7 @@ public class AttachmentController: ViewController {
|
||||
|
||||
self.container = AttachmentContainer()
|
||||
self.container.canHaveKeyboardFocus = true
|
||||
self.panel = AttachmentPanel(context: controller.context, chatLocation: controller.chatLocation, updatedPresentationData: controller.updatedPresentationData)
|
||||
self.panel = AttachmentPanel(context: controller.context, chatLocation: controller.chatLocation, updatedPresentationData: controller.updatedPresentationData, makeEntityInputView: makeEntityInputView)
|
||||
self.panel.fromMenu = controller.fromMenu
|
||||
self.panel.isStandalone = controller.isStandalone
|
||||
|
||||
@ -843,13 +847,14 @@ public class AttachmentController: ViewController {
|
||||
|
||||
public var getInputContainerNode: () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil }
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false) {
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
|
||||
self.context = context
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
self.chatLocation = chatLocation
|
||||
self.buttons = buttons
|
||||
self.initialButton = initialButton
|
||||
self.fromMenu = fromMenu
|
||||
self.makeEntityInputView = makeEntityInputView
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
@ -877,7 +882,7 @@ public class AttachmentController: ViewController {
|
||||
}
|
||||
|
||||
open override func loadDisplayNode() {
|
||||
self.displayNode = Node(controller: self)
|
||||
self.displayNode = Node(controller: self, makeEntityInputView: self.makeEntityInputView)
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
|
||||
@ -458,6 +458,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
private var presentationInterfaceState: ChatPresentationInterfaceState
|
||||
private var interfaceInteraction: ChatPanelInterfaceInteraction?
|
||||
|
||||
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
|
||||
|
||||
private let containerNode: ASDisplayNode
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
private let scrollNode: ASScrollNode
|
||||
@ -496,9 +498,11 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
var mainButtonPressed: () -> Void = { }
|
||||
|
||||
init(context: AccountContext, chatLocation: ChatLocation, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?) {
|
||||
init(context: AccountContext, chatLocation: ChatLocation, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
|
||||
self.context = context
|
||||
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.makeEntityInputView = makeEntityInputView
|
||||
|
||||
self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil)
|
||||
|
||||
@ -895,7 +899,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(c)
|
||||
}
|
||||
})
|
||||
}, makeEntityInputView: self.makeEntityInputView)
|
||||
textInputPanelNode.interfaceInteraction = self.interfaceInteraction
|
||||
textInputPanelNode.sendMessage = { [weak self] mode in
|
||||
if let strongSelf = self {
|
||||
|
||||
@ -543,7 +543,11 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
||||
case .hide:
|
||||
effectiveTopPanelHeight = 0.0
|
||||
case .show, .hideOnScroll:
|
||||
effectiveTopPanelHeight = topPanelHeight
|
||||
if component.externalTopPanelContainer != nil {
|
||||
effectiveTopPanelHeight = topPanelHeight
|
||||
} else {
|
||||
effectiveTopPanelHeight = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
if let contentBackground = component.contentBackground {
|
||||
|
||||
@ -41,6 +41,9 @@ swift_library(
|
||||
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
|
||||
"//submodules/TranslateUI:TranslateUI",
|
||||
"//submodules/Utils/RangeSet:RangeSet",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -22,6 +22,9 @@ import UndoUI
|
||||
import ManagedAnimationNode
|
||||
import TelegramUniversalVideoContent
|
||||
import InvisibleInkDustNode
|
||||
import TextNodeWithEntities
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
|
||||
private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white)
|
||||
private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white)
|
||||
@ -133,10 +136,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
private let scrollWrapperNode: CaptionScrollWrapperNode
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
private let textNode: ImmediateTextNode
|
||||
private var spoilerTextNode: ImmediateTextNode?
|
||||
private let textNode: ImmediateTextNodeWithEntities
|
||||
private var spoilerTextNode: ImmediateTextNodeWithEntities?
|
||||
private var dustNode: InvisibleInkDustNode?
|
||||
|
||||
private let animationCache: AnimationCache
|
||||
private let animationRenderer: MultiAnimationRenderer
|
||||
|
||||
private let authorNameNode: ASTextNode
|
||||
private let dateNode: ASTextNode
|
||||
private let backwardButton: PlaybackButtonNode
|
||||
@ -319,7 +325,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
|
||||
self.maskNode = ASDisplayNode()
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode = ImmediateTextNodeWithEntities()
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
self.textNode.linkHighlightColor = UIColor(rgb: 0x5ac8fa, alpha: 0.2)
|
||||
|
||||
@ -350,6 +356,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
self.statusNode.isUserInteractionEnabled = false
|
||||
|
||||
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
|
||||
return TempBox.shared.tempFile(fileName: "file").path
|
||||
})
|
||||
self.animationRenderer = MultiAnimationRendererImpl()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.contentNode)
|
||||
@ -380,6 +391,15 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
}
|
||||
}
|
||||
|
||||
self.textNode.arguments = TextNodeWithEntities.Arguments(
|
||||
context: self.context,
|
||||
cache: self.animationCache,
|
||||
renderer: self.animationRenderer,
|
||||
placeholderColor: defaultDarkPresentationTheme.list.mediaPlaceholderColor,
|
||||
attemptSynchronous: false
|
||||
)
|
||||
self.textNode.visibility = true
|
||||
|
||||
self.contentNode.view.addSubview(self.deleteButton)
|
||||
self.contentNode.view.addSubview(self.fullscreenButton)
|
||||
self.contentNode.view.addSubview(self.actionButton)
|
||||
@ -723,7 +743,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
private func updateSpoilers(textFrame: CGRect) {
|
||||
if let textLayout = self.textNode.cachedLayout, !textLayout.spoilers.isEmpty {
|
||||
if self.spoilerTextNode == nil {
|
||||
let spoilerTextNode = ImmediateTextNode()
|
||||
let spoilerTextNode = ImmediateTextNodeWithEntities()
|
||||
spoilerTextNode.attributedText = textNode.attributedText
|
||||
spoilerTextNode.maximumNumberOfLines = 0
|
||||
spoilerTextNode.linkHighlightColor = UIColor(rgb: 0x5ac8fa, alpha: 0.2)
|
||||
|
||||
@ -85,7 +85,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
public let hideInputUpdated: (Bool, Bool, Transition) -> Void
|
||||
public let switchToTextInput: () -> Void
|
||||
public let switchToGifSubject: (GifPagerContentComponent.Subject) -> Void
|
||||
public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode
|
||||
public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode?
|
||||
public let deviceMetrics: DeviceMetrics
|
||||
public let hiddenInputHeight: CGFloat
|
||||
public let isExpanded: Bool
|
||||
@ -103,7 +103,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void,
|
||||
switchToTextInput: @escaping () -> Void,
|
||||
switchToGifSubject: @escaping (GifPagerContentComponent.Subject) -> Void,
|
||||
makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode,
|
||||
makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode?,
|
||||
deviceMetrics: DeviceMetrics,
|
||||
hiddenInputHeight: CGFloat,
|
||||
isExpanded: Bool
|
||||
@ -486,7 +486,8 @@ public final class EntityKeyboardComponent: Component {
|
||||
)),
|
||||
topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent(
|
||||
theme: component.theme,
|
||||
overflowHeight: component.hiddenInputHeight
|
||||
overflowHeight: component.hiddenInputHeight,
|
||||
displayBackground: component.externalTopPanelContainer == nil
|
||||
)),
|
||||
externalTopPanelContainer: component.externalTopPanelContainer,
|
||||
bottomPanel: AnyComponent(EntityKeyboardBottomPanelComponent(
|
||||
|
||||
@ -278,59 +278,64 @@ final class EntityKeyboardBottomPanelComponent: Component {
|
||||
|
||||
let navigateToContentId = panelEnvironment.navigateToContentId
|
||||
|
||||
for icon in panelEnvironment.contentIcons {
|
||||
validIconIds.append(icon.id)
|
||||
|
||||
var iconTransition = transition
|
||||
let iconView: ComponentHostView<Empty>
|
||||
if let current = self.iconViews[icon.id] {
|
||||
iconView = current
|
||||
} else {
|
||||
iconTransition = .immediate
|
||||
iconView = ComponentHostView<Empty>()
|
||||
self.iconViews[icon.id] = iconView
|
||||
self.addSubview(iconView)
|
||||
if panelEnvironment.contentIcons.count > 1 {
|
||||
for icon in panelEnvironment.contentIcons {
|
||||
validIconIds.append(icon.id)
|
||||
|
||||
var iconTransition = transition
|
||||
let iconView: ComponentHostView<Empty>
|
||||
if let current = self.iconViews[icon.id] {
|
||||
iconView = current
|
||||
} else {
|
||||
iconTransition = .immediate
|
||||
iconView = ComponentHostView<Empty>()
|
||||
self.iconViews[icon.id] = iconView
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
|
||||
let iconSize = iconView.update(
|
||||
transition: iconTransition,
|
||||
component: AnyComponent(BottomPanelIconComponent(
|
||||
content: icon.component,
|
||||
action: {
|
||||
navigateToContentId(icon.id)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
|
||||
iconInfos[icon.id] = (size: iconSize, transition: iconTransition)
|
||||
|
||||
if !iconTotalSize.width.isZero {
|
||||
iconTotalSize.width += iconSpacing
|
||||
}
|
||||
iconTotalSize.width += iconSize.width
|
||||
iconTotalSize.height = max(iconTotalSize.height, iconSize.height)
|
||||
}
|
||||
|
||||
let iconSize = iconView.update(
|
||||
transition: iconTransition,
|
||||
component: AnyComponent(BottomPanelIconComponent(
|
||||
content: icon.component,
|
||||
action: {
|
||||
navigateToContentId(icon.id)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
|
||||
iconInfos[icon.id] = (size: iconSize, transition: iconTransition)
|
||||
|
||||
if !iconTotalSize.width.isZero {
|
||||
iconTotalSize.width += iconSpacing
|
||||
}
|
||||
iconTotalSize.width += iconSize.width
|
||||
iconTotalSize.height = max(iconTotalSize.height, iconSize.height)
|
||||
}
|
||||
|
||||
var nextIconOrigin = CGPoint(x: floor((availableSize.width - iconTotalSize.width) / 2.0), y: floor((intrinsicHeight - iconTotalSize.height) / 2.0))
|
||||
if component.bottomInset > 0.0 {
|
||||
nextIconOrigin.y += 2.0
|
||||
}
|
||||
for icon in panelEnvironment.contentIcons {
|
||||
guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else {
|
||||
continue
|
||||
|
||||
if panelEnvironment.contentIcons.count > 1 {
|
||||
for icon in panelEnvironment.contentIcons {
|
||||
guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else {
|
||||
continue
|
||||
}
|
||||
|
||||
let iconFrame = CGRect(origin: nextIconOrigin, size: iconInfo.size)
|
||||
iconInfo.transition.setFrame(view: iconView, frame: iconFrame, completion: nil)
|
||||
|
||||
if let activeContentId = activeContentId, activeContentId == icon.id {
|
||||
self.highlightedIconBackgroundView.isHidden = false
|
||||
transition.setFrame(view: self.highlightedIconBackgroundView, frame: iconFrame)
|
||||
}
|
||||
|
||||
nextIconOrigin.x += iconInfo.size.width + iconSpacing
|
||||
}
|
||||
|
||||
let iconFrame = CGRect(origin: nextIconOrigin, size: iconInfo.size)
|
||||
iconInfo.transition.setFrame(view: iconView, frame: iconFrame, completion: nil)
|
||||
|
||||
if let activeContentId = activeContentId, activeContentId == icon.id {
|
||||
self.highlightedIconBackgroundView.isHidden = false
|
||||
transition.setFrame(view: self.highlightedIconBackgroundView, frame: iconFrame)
|
||||
}
|
||||
|
||||
nextIconOrigin.x += iconInfo.size.width + iconSpacing
|
||||
}
|
||||
|
||||
if activeContentId == nil {
|
||||
|
||||
@ -32,13 +32,16 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
||||
|
||||
let theme: PresentationTheme
|
||||
let overflowHeight: CGFloat
|
||||
let displayBackground: Bool
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
overflowHeight: CGFloat
|
||||
overflowHeight: CGFloat,
|
||||
displayBackground: Bool
|
||||
) {
|
||||
self.theme = theme
|
||||
self.overflowHeight = overflowHeight
|
||||
self.displayBackground = displayBackground
|
||||
}
|
||||
|
||||
static func ==(lhs: EntityKeyboardTopContainerPanelComponent, rhs: EntityKeyboardTopContainerPanelComponent) -> Bool {
|
||||
@ -48,6 +51,9 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
||||
if lhs.overflowHeight != rhs.overflowHeight {
|
||||
return false
|
||||
}
|
||||
if lhs.displayBackground != rhs.displayBackground {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -59,6 +65,9 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var backgroundView: BlurredBackgroundView?
|
||||
private var backgroundSeparatorView: UIView?
|
||||
|
||||
private var panelViews: [AnyHashable: PanelView] = [:]
|
||||
|
||||
private var component: EntityKeyboardTopContainerPanelComponent?
|
||||
@ -183,6 +192,40 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
||||
strongSelf.updateVisibilityFraction(value: fraction, transition: transition)
|
||||
}
|
||||
|
||||
if component.displayBackground {
|
||||
let backgroundView: BlurredBackgroundView
|
||||
if let current = self.backgroundView {
|
||||
backgroundView = current
|
||||
} else {
|
||||
backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
self.insertSubview(backgroundView, at: 0)
|
||||
}
|
||||
|
||||
let backgroundSeparatorView: UIView
|
||||
if let current = self.backgroundSeparatorView {
|
||||
backgroundSeparatorView = current
|
||||
} else {
|
||||
backgroundSeparatorView = UIView()
|
||||
self.insertSubview(backgroundSeparatorView, aboveSubview: backgroundView)
|
||||
}
|
||||
|
||||
backgroundView.updateColor(color: component.theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(1.0), transition: .immediate)
|
||||
backgroundView.update(size: CGSize(width: availableSize.width, height: height), transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)))
|
||||
|
||||
backgroundSeparatorView.backgroundColor = component.theme.chat.inputPanel.panelSeparatorColor
|
||||
transition.setFrame(view: backgroundSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: height), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||
} else {
|
||||
if let backgroundView = self.backgroundView {
|
||||
self.backgroundView = nil
|
||||
backgroundView.removeFromSuperview()
|
||||
}
|
||||
if let backgroundSeparatorView = self.backgroundSeparatorView {
|
||||
self.backgroundSeparatorView = nil
|
||||
backgroundSeparatorView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
|
||||
|
||||
@ -51,11 +51,11 @@ final class EntitySearchContentEnvironment: Equatable {
|
||||
final class EntitySearchContentComponent: Component {
|
||||
typealias EnvironmentType = EntitySearchContentEnvironment
|
||||
|
||||
let makeContainerNode: () -> EntitySearchContainerNode
|
||||
let makeContainerNode: () -> EntitySearchContainerNode?
|
||||
let dismissSearch: () -> Void
|
||||
|
||||
init(
|
||||
makeContainerNode: @escaping () -> EntitySearchContainerNode,
|
||||
makeContainerNode: @escaping () -> EntitySearchContainerNode?,
|
||||
dismissSearch: @escaping () -> Void
|
||||
) {
|
||||
self.makeContainerNode = makeContainerNode
|
||||
@ -78,30 +78,34 @@ final class EntitySearchContentComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: EntitySearchContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
let containerNode: EntitySearchContainerNode
|
||||
let containerNode: EntitySearchContainerNode?
|
||||
if let current = self.containerNode {
|
||||
containerNode = current
|
||||
} else {
|
||||
containerNode = component.makeContainerNode()
|
||||
self.containerNode = containerNode
|
||||
self.addSubnode(containerNode)
|
||||
if let containerNode = containerNode {
|
||||
self.containerNode = containerNode
|
||||
self.addSubnode(containerNode)
|
||||
}
|
||||
}
|
||||
|
||||
if let containerNode = containerNode {
|
||||
|
||||
let environmentValue = environment[EntitySearchContentEnvironment.self].value
|
||||
|
||||
transition.setFrame(view: containerNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
containerNode.updateLayout(
|
||||
size: availableSize,
|
||||
leftInset: 0.0,
|
||||
rightInset: 0.0,
|
||||
bottomInset: 0.0,
|
||||
inputHeight: 0.0,
|
||||
deviceMetrics: environmentValue.deviceMetrics,
|
||||
transition: transition.containedViewLayoutTransition
|
||||
)
|
||||
|
||||
containerNode.onCancel = {
|
||||
component.dismissSearch()
|
||||
transition.setFrame(view: containerNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
containerNode.updateLayout(
|
||||
size: availableSize,
|
||||
leftInset: 0.0,
|
||||
rightInset: 0.0,
|
||||
bottomInset: 0.0,
|
||||
inputHeight: 0.0,
|
||||
deviceMetrics: environmentValue.deviceMetrics,
|
||||
transition: transition.containedViewLayoutTransition
|
||||
)
|
||||
|
||||
containerNode.onCancel = {
|
||||
component.dismissSearch()
|
||||
}
|
||||
}
|
||||
|
||||
return availableSize
|
||||
|
||||
@ -118,7 +118,7 @@ public final class TextNodeWithEntities {
|
||||
if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont {
|
||||
string.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: range)
|
||||
|
||||
let itemSize = font.pointSize * 24.0 / 17.0 / CGFloat(range.length)
|
||||
let itemSize = font.pointSize * 24.0 / 17.0 / CGFloat(min(2, range.length))
|
||||
|
||||
let runDelegateData = RunDelegateData(
|
||||
ascent: font.ascender,
|
||||
|
||||
@ -10732,7 +10732,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
})
|
||||
|
||||
let inputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { _ in })
|
||||
let inputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { _ in }, makeEntityInputView: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return EntityInputView(context: strongSelf.context, isDark: true)
|
||||
})
|
||||
inputPanelNode.interfaceInteraction = interfaceInteraction
|
||||
inputPanelNode.effectivePresentationInterfaceState = {
|
||||
return presentationInterfaceState
|
||||
@ -10967,7 +10973,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
let currentFilesController = Atomic<AttachmentContainable?>(value: nil)
|
||||
let currentLocationController = Atomic<AttachmentContainable?>(value: nil)
|
||||
|
||||
let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: strongSelf.chatLocation, buttons: buttons, initialButton: initialButton)
|
||||
let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: strongSelf.chatLocation, buttons: buttons, initialButton: initialButton, makeEntityInputView: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return nil
|
||||
}
|
||||
return EntityInputView(context: strongSelf.context, isDark: false)
|
||||
})
|
||||
attachmentController.requestController = { [weak self, weak attachmentController] type, completion in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
||||
@ -540,14 +540,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
self.addSubnode(self.inputPanelContainerNode)
|
||||
self.addSubnode(self.inputContextPanelContainer)
|
||||
|
||||
self.inputPanelContainerNode.addSubnode(self.inputPanelClippingNode)
|
||||
self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundNode)
|
||||
self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundSeparatorNode)
|
||||
self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode)
|
||||
|
||||
self.contentContainerNode.addSubnode(self.inputContextPanelContainer)
|
||||
|
||||
self.addSubnode(self.messageTransitionNode)
|
||||
self.contentContainerNode.addSubnode(self.navigateButtons)
|
||||
self.contentContainerNode.addSubnode(self.presentationContextMarker)
|
||||
|
||||
@ -21,6 +21,8 @@ import AudioToolbox
|
||||
import UndoUI
|
||||
import ContextUI
|
||||
import GalleryUI
|
||||
import AttachmentTextInputPanelNode
|
||||
import TelegramPresentationData
|
||||
|
||||
private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = {
|
||||
guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else {
|
||||
@ -46,14 +48,14 @@ private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment,
|
||||
final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
struct InputData: Equatable {
|
||||
var emoji: EmojiPagerContentComponent
|
||||
var stickers: EmojiPagerContentComponent
|
||||
var gifs: GifPagerContentComponent
|
||||
var stickers: EmojiPagerContentComponent?
|
||||
var gifs: GifPagerContentComponent?
|
||||
var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
|
||||
|
||||
init(
|
||||
emoji: EmojiPagerContentComponent,
|
||||
stickers: EmojiPagerContentComponent,
|
||||
gifs: GifPagerContentComponent,
|
||||
stickers: EmojiPagerContentComponent?,
|
||||
gifs: GifPagerContentComponent?,
|
||||
availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
|
||||
) {
|
||||
self.emoji = emoji
|
||||
@ -63,7 +65,109 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
}
|
||||
|
||||
static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction, chatPeerId: PeerId?) -> Signal<InputData, NoError> {
|
||||
static func emojiInputData(context: AccountContext, inputInteraction: EmojiPagerContentComponent.InputInteraction, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError> {
|
||||
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> map { peer -> Bool in
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
return user.isPremium
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
||||
|
||||
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||
hasPremium
|
||||
)
|
||||
|> map { view, hasPremium -> EmojiPagerContentComponent in
|
||||
struct ItemGroup {
|
||||
var supergroupId: AnyHashable
|
||||
var id: AnyHashable
|
||||
var isPremium: Bool
|
||||
var items: [EmojiPagerContentComponent.Item]
|
||||
}
|
||||
var itemGroups: [ItemGroup] = []
|
||||
var itemGroupIndexById: [AnyHashable: Int] = [:]
|
||||
|
||||
for (subgroupId, list) in staticEmojiMapping {
|
||||
let groupId: AnyHashable = "static"
|
||||
for emojiString in list {
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
file: nil,
|
||||
staticEmoji: emojiString,
|
||||
subgroupId: subgroupId.rawValue
|
||||
)
|
||||
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
itemGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
itemGroupIndexById[groupId] = itemGroups.count
|
||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, isPremium: false, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for entry in view.entries {
|
||||
guard let item = entry.item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
file: item.file,
|
||||
staticEmoji: nil,
|
||||
subgroupId: nil
|
||||
)
|
||||
|
||||
let supergroupId = entry.index.collectionId
|
||||
let groupId: AnyHashable = supergroupId
|
||||
let isPremium: Bool = item.file.isPremiumEmoji && !hasPremium
|
||||
if isPremium && isPremiumDisabled {
|
||||
continue
|
||||
}
|
||||
/*if isPremium {
|
||||
groupId = "\(supergroupId)-p"
|
||||
} else {
|
||||
groupId = supergroupId
|
||||
}*/
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
itemGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
itemGroupIndexById[groupId] = itemGroups.count
|
||||
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, isPremium: isPremium, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
|
||||
return EmojiPagerContentComponent(
|
||||
id: "emoji",
|
||||
context: context,
|
||||
animationCache: animationCache,
|
||||
animationRenderer: animationRenderer,
|
||||
inputInteraction: inputInteraction,
|
||||
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
|
||||
var title: String?
|
||||
if group.id == AnyHashable("recent") {
|
||||
//TODO:localize
|
||||
title = "Recently Used"
|
||||
} else {
|
||||
for (id, info, _) in view.collectionInfos {
|
||||
if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo {
|
||||
title = info.title
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: title, isPremium: group.isPremium, displayPremiumBadges: false, items: group.items)
|
||||
},
|
||||
itemLayoutType: .compact
|
||||
)
|
||||
}
|
||||
return emojiItems
|
||||
}
|
||||
|
||||
static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?) -> Signal<InputData, NoError> {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
||||
|
||||
@ -177,9 +281,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
chatPeerId: chatPeerId
|
||||
)
|
||||
let stickerInputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak interfaceInteraction] item, view, rect, layer in
|
||||
performItemAction: { [weak controllerInteraction, weak interfaceInteraction] item, view, rect, layer in
|
||||
let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
|
||||
guard let interfaceInteraction = interfaceInteraction else {
|
||||
guard let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
|
||||
return
|
||||
}
|
||||
if let file = item.file {
|
||||
@ -249,98 +353,13 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
animationRenderer = MultiAnimationRendererImpl()
|
||||
//}
|
||||
|
||||
let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
|
||||
let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
|
||||
let emojiItems = emojiInputData(context: context, inputInteraction: emojiInputInteraction, animationCache: animationCache, animationRenderer: animationRenderer)
|
||||
|
||||
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||
hasPremium
|
||||
)
|
||||
|> map { view, hasPremium -> EmojiPagerContentComponent in
|
||||
struct ItemGroup {
|
||||
var supergroupId: AnyHashable
|
||||
var id: AnyHashable
|
||||
var isPremium: Bool
|
||||
var items: [EmojiPagerContentComponent.Item]
|
||||
}
|
||||
var itemGroups: [ItemGroup] = []
|
||||
var itemGroupIndexById: [AnyHashable: Int] = [:]
|
||||
|
||||
for (subgroupId, list) in staticEmojiMapping {
|
||||
let groupId: AnyHashable = "static"
|
||||
for emojiString in list {
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
file: nil,
|
||||
staticEmoji: emojiString,
|
||||
subgroupId: subgroupId.rawValue
|
||||
)
|
||||
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
itemGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
itemGroupIndexById[groupId] = itemGroups.count
|
||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, isPremium: false, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for entry in view.entries {
|
||||
guard let item = entry.item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
file: item.file,
|
||||
staticEmoji: nil,
|
||||
subgroupId: nil
|
||||
)
|
||||
|
||||
let supergroupId = entry.index.collectionId
|
||||
let groupId: AnyHashable = supergroupId
|
||||
let isPremium: Bool = item.file.isPremiumEmoji && !hasPremium
|
||||
if isPremium && isPremiumDisabled {
|
||||
continue
|
||||
}
|
||||
/*if isPremium {
|
||||
groupId = "\(supergroupId)-p"
|
||||
} else {
|
||||
groupId = supergroupId
|
||||
}*/
|
||||
if let groupIndex = itemGroupIndexById[groupId] {
|
||||
itemGroups[groupIndex].items.append(resultItem)
|
||||
} else {
|
||||
itemGroupIndexById[groupId] = itemGroups.count
|
||||
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, isPremium: isPremium, items: [resultItem]))
|
||||
}
|
||||
}
|
||||
|
||||
return EmojiPagerContentComponent(
|
||||
id: "emoji",
|
||||
context: context,
|
||||
animationCache: animationCache,
|
||||
animationRenderer: animationRenderer,
|
||||
inputInteraction: emojiInputInteraction,
|
||||
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
|
||||
var title: String?
|
||||
if group.id == AnyHashable("recent") {
|
||||
//TODO:localize
|
||||
title = "Recently Used"
|
||||
} else {
|
||||
for (id, info, _) in view.collectionInfos {
|
||||
if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo {
|
||||
title = info.title
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: title, isPremium: group.isPremium, displayPremiumBadges: false, items: group.items)
|
||||
},
|
||||
itemLayoutType: .compact
|
||||
)
|
||||
}
|
||||
let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
|
||||
let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
|
||||
|
||||
let stickerItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000),
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000),
|
||||
hasPremium
|
||||
)
|
||||
|> map { view, hasPremium -> EmojiPagerContentComponent in
|
||||
@ -604,7 +623,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
private var currentInputData: InputData
|
||||
private var inputDataDisposable: Disposable?
|
||||
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private let controllerInteraction: ChatControllerInteraction?
|
||||
|
||||
private var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
|
||||
@ -617,6 +636,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
return self.externalTopPanelContainerImpl
|
||||
}
|
||||
|
||||
var switchToTextInput: (() -> Void)?
|
||||
|
||||
private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)?
|
||||
|
||||
private var gifMode: GifPagerContentComponent.Subject = .recent {
|
||||
@ -811,7 +832,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
private let gifComponent = Promise<GifPagerContentComponent>()
|
||||
private var gifInputInteraction: GifPagerContentComponent.InputInteraction?
|
||||
|
||||
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction) {
|
||||
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction?) {
|
||||
self.context = context
|
||||
self.currentInputData = currentInputData
|
||||
self.defaultToEmojiTab = defaultToEmojiTab
|
||||
@ -898,6 +919,15 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
)
|
||||
|
||||
self.switchToTextInput = { [weak self] in
|
||||
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
|
||||
return
|
||||
}
|
||||
controllerInteraction.updateInputMode { _ in
|
||||
return .text
|
||||
}
|
||||
}
|
||||
|
||||
self.reloadGifContext()
|
||||
}
|
||||
|
||||
@ -996,12 +1026,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
},
|
||||
switchToTextInput: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerInteraction.updateInputMode { _ in
|
||||
return .text
|
||||
}
|
||||
self?.switchToTextInput?()
|
||||
},
|
||||
switchToGifSubject: { [weak self] subject in
|
||||
guard let strongSelf = self else {
|
||||
@ -1009,7 +1034,11 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
strongSelf.gifMode = subject
|
||||
},
|
||||
makeSearchContainerNode: { content in
|
||||
makeSearchContainerNode: { [weak controllerInteraction] content in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let mappedMode: ChatMediaInputSearchMode
|
||||
switch content {
|
||||
case .stickers:
|
||||
@ -1074,7 +1103,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
if isSaved {
|
||||
let _ = self?.controllerInteraction.sendGif(FileMediaReference.savedGif(media: file), sourceView, sourceRect, false, false)
|
||||
let _ = self?.controllerInteraction?.sendGif(FileMediaReference.savedGif(media: file), sourceView, sourceRect, false, false)
|
||||
}/* else if let (collection, result) = file.contextResult {
|
||||
let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false)
|
||||
}*/
|
||||
@ -1141,7 +1170,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|> deliverOnMainQueue).start(next: { result in
|
||||
switch result {
|
||||
case .generic:
|
||||
controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
case let .limitExceeded(limit, premiumLimit):
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
let text: String
|
||||
@ -1150,10 +1179,10 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
} else {
|
||||
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string
|
||||
}
|
||||
controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in
|
||||
controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in
|
||||
if case .info = action {
|
||||
let controller = PremiumIntroScreen(context: context, source: .savedGifs)
|
||||
controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
controllerInteraction?.navigationController()?.pushViewController(controller)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -1164,7 +1193,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
strongSelf.controllerInteraction.presentGlobalOverlayController(contextController, nil)
|
||||
strongSelf.controllerInteraction?.presentGlobalOverlayController(contextController, nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1202,3 +1231,220 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback {
|
||||
private let context: AccountContext
|
||||
|
||||
public var insertText: ((NSAttributedString) -> Void)?
|
||||
public var deleteBackwards: (() -> Void)?
|
||||
public var switchToKeyboard: (() -> Void)?
|
||||
public var presentController: ((ViewController) -> Void)?
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var inputNode: ChatEntityKeyboardInputNode?
|
||||
private let animationCache: AnimationCache
|
||||
private let animationRenderer: MultiAnimationRenderer
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
isDark: Bool
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
|
||||
return TempBox.shared.tempFile(fileName: "file").path
|
||||
})
|
||||
self.animationRenderer = MultiAnimationRendererImpl()
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
if isDark {
|
||||
self.presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme)
|
||||
}
|
||||
|
||||
//super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), inputViewStyle: .default)
|
||||
super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)))
|
||||
|
||||
self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.clipsToBounds = true
|
||||
|
||||
let inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak self] item, _, _, _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let hasPremium = strongSelf.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId))
|
||||
|> map { peer -> Bool in
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
return user.isPremium
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let file = item.file {
|
||||
var text = "."
|
||||
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 file.isPremiumEmoji && !hasPremium {
|
||||
//TODO:localize
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.presentController?(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: "Subscribe to Telegram Premium to unlock this emoji.", undoText: "More", customAction: {
|
||||
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.presentController?(controller)
|
||||
}), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }))
|
||||
return
|
||||
}
|
||||
|
||||
if let emojiAttribute = emojiAttribute {
|
||||
AudioServicesPlaySystemSound(0x450)
|
||||
strongSelf.insertText?(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]))
|
||||
}
|
||||
} else if let staticEmoji = item.staticEmoji {
|
||||
AudioServicesPlaySystemSound(0x450)
|
||||
strongSelf.insertText?(NSAttributedString(string: staticEmoji, attributes: [:]))
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteBackwards: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.deleteBackwards?()
|
||||
},
|
||||
openStickerSettings: {
|
||||
},
|
||||
openPremiumSection: {
|
||||
},
|
||||
pushController: { _ in
|
||||
},
|
||||
presentController: { _ in
|
||||
},
|
||||
presentGlobalOverlayController: { _ in
|
||||
},
|
||||
navigationController: {
|
||||
return nil
|
||||
},
|
||||
sendSticker: nil,
|
||||
chatPeerId: nil
|
||||
)
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var emojiComponent: EmojiPagerContentComponent?
|
||||
let _ = ChatEntityKeyboardInputNode.emojiInputData(context: context, inputInteraction: inputInteraction, animationCache: self.animationCache, animationRenderer: self.animationRenderer).start(next: { value in
|
||||
emojiComponent = value
|
||||
semaphore.signal()
|
||||
})
|
||||
semaphore.wait()
|
||||
|
||||
if let emojiComponent = emojiComponent {
|
||||
let inputNode = ChatEntityKeyboardInputNode(
|
||||
context: self.context,
|
||||
currentInputData: ChatEntityKeyboardInputNode.InputData(
|
||||
emoji: emojiComponent,
|
||||
stickers: nil,
|
||||
gifs: nil,
|
||||
availableGifSearchEmojies: []
|
||||
),
|
||||
updatedInputData: .never(),
|
||||
defaultToEmojiTab: true,
|
||||
controllerInteraction: nil
|
||||
)
|
||||
self.inputNode = inputNode
|
||||
inputNode.externalTopPanelContainerImpl = nil
|
||||
inputNode.switchToTextInput = { [weak self] in
|
||||
self?.switchToKeyboard?()
|
||||
}
|
||||
self.addSubnode(inputNode)
|
||||
}
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
guard let inputNode = self.inputNode else {
|
||||
return
|
||||
}
|
||||
|
||||
for view in self.subviews {
|
||||
if view !== inputNode.view {
|
||||
view.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
let bottomInset: CGFloat
|
||||
if #available(iOS 11.0, *) {
|
||||
bottomInset = max(0.0, UIScreen.main.bounds.height - (self.window?.safeAreaLayoutGuide.layoutFrame.maxY ?? 10000.0))
|
||||
} else {
|
||||
bottomInset = 0.0
|
||||
}
|
||||
|
||||
let presentationInterfaceState = ChatPresentationInterfaceState(
|
||||
chatWallpaper: .builtin(WallpaperSettings()),
|
||||
theme: self.presentationData.theme,
|
||||
strings: self.presentationData.strings,
|
||||
dateTimeFormat: self.presentationData.dateTimeFormat,
|
||||
nameDisplayOrder: self.presentationData.nameDisplayOrder,
|
||||
limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 },
|
||||
fontSize: self.presentationData.chatFontSize,
|
||||
bubbleCorners: self.presentationData.chatBubbleCorners,
|
||||
accountPeerId: self.context.account.peerId,
|
||||
mode: .standard(previewing: false),
|
||||
chatLocation: .peer(id: self.context.account.peerId),
|
||||
subject: nil,
|
||||
peerNearbyData: nil,
|
||||
greetingData: nil,
|
||||
pendingUnpinnedAllMessages: false,
|
||||
activeGroupCallInfo: nil,
|
||||
hasActiveGroupCall: false,
|
||||
importState: nil
|
||||
)
|
||||
|
||||
let _ = inputNode.updateLayout(
|
||||
width: self.bounds.width,
|
||||
leftInset: 0.0,
|
||||
rightInset: 0.0,
|
||||
bottomInset: bottomInset,
|
||||
standardInputHeight: self.bounds.height,
|
||||
inputHeight: self.bounds.height,
|
||||
maximumHeight: self.bounds.height,
|
||||
inputPanelHeight: 0.0,
|
||||
transition: .immediate,
|
||||
interfaceState: presentationInterfaceState,
|
||||
deviceMetrics: DeviceMetrics.iPhone12,
|
||||
isVisible: true,
|
||||
isExpanded: false
|
||||
)
|
||||
inputNode.frame = self.bounds
|
||||
}
|
||||
}
|
||||
|
||||
@ -390,7 +390,9 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
self.addSubnode(forwardAccessoryPanelNode)
|
||||
self.forwardAccessoryPanelNode = forwardAccessoryPanelNode
|
||||
|
||||
let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, presentController: { [weak self] c in self?.present(c, nil) })
|
||||
let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, presentController: { [weak self] c in self?.present(c, nil) }, makeEntityInputView: {
|
||||
return nil
|
||||
})
|
||||
textInputPanelNode.interfaceInteraction = self.interfaceInteraction
|
||||
textInputPanelNode.sendMessage = { [weak self] mode in
|
||||
guard let strongSelf = self else {
|
||||
|
||||
@ -70,6 +70,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
|
||||
self.textNode.visibility = true
|
||||
|
||||
if let animationCache = animationCache, let animationRenderer = animationRenderer {
|
||||
self.textNode.arguments = TextNodeWithEntities.Arguments(
|
||||
|
||||
@ -1212,7 +1212,9 @@ private final class WebAppContextReferenceContentSource: ContextReferenceContent
|
||||
}
|
||||
|
||||
public func standaloneWebAppController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, params: WebAppParameters, openUrl: @escaping (String) -> Void, getInputContainerNode: @escaping () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil }, completion: @escaping () -> Void = {}, willDismiss: @escaping () -> Void = {}, didDismiss: @escaping () -> Void = {}, getNavigationController: @escaping () -> NavigationController? = { return nil }) -> ViewController {
|
||||
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.fromMenu)
|
||||
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.fromMenu, makeEntityInputView: {
|
||||
return nil
|
||||
})
|
||||
controller.getInputContainerNode = getInputContainerNode
|
||||
controller.requestController = { _, present in
|
||||
let webAppController = WebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user