mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-07 01:10:09 +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/Pasteboard:Pasteboard",
|
||||||
"//submodules/ContextUI:ContextUI",
|
"//submodules/ContextUI:ContextUI",
|
||||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
"//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 = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -19,6 +19,11 @@ import TextInputMenu
|
|||||||
import ChatPresentationInterfaceState
|
import ChatPresentationInterfaceState
|
||||||
import Pasteboard
|
import Pasteboard
|
||||||
import EmojiTextAttachmentView
|
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 counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
|
||||||
private let minInputFontSize: CGFloat = 5.0
|
private let minInputFontSize: CGFloat = 5.0
|
||||||
@ -64,7 +69,7 @@ private func calculateTextFieldRealInsets(_ presentationInterfaceState: ChatPres
|
|||||||
top = 0.0
|
top = 0.0
|
||||||
bottom = 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)?
|
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 {
|
private class CaptionEditableTextNode: EditableTextNode {
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
let previousAlpha = self.alpha
|
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 {
|
public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, ASEditableTextNodeDelegate {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
|
|
||||||
private let isCaption: Bool
|
private let isCaption: Bool
|
||||||
private let isAttachment: Bool
|
private let isAttachment: Bool
|
||||||
|
|
||||||
|
private let presentController: (ViewController) -> Void
|
||||||
|
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
|
||||||
|
|
||||||
private var textPlaceholderNode: ImmediateTextNode
|
private var textPlaceholderNode: ImmediateTextNode
|
||||||
private let textInputContainerBackgroundNode: ASImageNode
|
private let textInputContainerBackgroundNode: ASImageNode
|
||||||
private let textInputContainer: ASDisplayNode
|
private let textInputContainer: ASDisplayNode
|
||||||
public var textInputNode: EditableTextNode?
|
public var textInputNode: EditableTextNode?
|
||||||
private var dustNode: InvisibleInkDustNode?
|
private var dustNode: InvisibleInkDustNode?
|
||||||
private var oneLineNode: ImmediateTextNode
|
private var customEmojiContainerView: CustomEmojiContainerView?
|
||||||
|
private var oneLineNode: TextNodeWithEntities
|
||||||
|
private var oneLineNodeAttributedText: NSAttributedString?
|
||||||
private var oneLineDustNode: InvisibleInkDustNode?
|
private var oneLineDustNode: InvisibleInkDustNode?
|
||||||
|
|
||||||
let textInputBackgroundNode: ASDisplayNode
|
let textInputBackgroundNode: ASDisplayNode
|
||||||
@ -164,6 +215,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
private var transparentTextInputBackgroundImage: UIImage?
|
private var transparentTextInputBackgroundImage: UIImage?
|
||||||
private let actionButtons: AttachmentTextInputActionButtonsNode
|
private let actionButtons: AttachmentTextInputActionButtonsNode
|
||||||
private let counterTextNode: ImmediateTextNode
|
private let counterTextNode: ImmediateTextNode
|
||||||
|
|
||||||
|
private let inputModeView: ComponentHostView<Empty>
|
||||||
|
|
||||||
private var validLayout: (CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool)?
|
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 spoilersRevealed = false
|
||||||
|
|
||||||
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
||||||
|
private let animationCache: AnimationCache
|
||||||
|
private let animationRenderer: MultiAnimationRenderer
|
||||||
|
|
||||||
private var maxCaptionLength: Int32?
|
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.context = context
|
||||||
self.presentationInterfaceState = presentationInterfaceState
|
self.presentationInterfaceState = presentationInterfaceState
|
||||||
self.isCaption = isCaption
|
self.isCaption = isCaption
|
||||||
self.isAttachment = isAttachment
|
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
|
var hasSpoilers = true
|
||||||
if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
|
if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
|
||||||
@ -293,6 +355,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
if !isCaption {
|
if !isCaption {
|
||||||
self.textInputContainer.addSubnode(self.textInputContainerBackgroundNode)
|
self.textInputContainer.addSubnode(self.textInputContainerBackgroundNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.inputModeView = ComponentHostView<Empty>()
|
||||||
|
self.textInputContainer.view.addSubview(self.inputModeView)
|
||||||
self.textInputContainer.clipsToBounds = true
|
self.textInputContainer.clipsToBounds = true
|
||||||
|
|
||||||
self.textInputBackgroundNode = ASDisplayNode()
|
self.textInputBackgroundNode = ASDisplayNode()
|
||||||
@ -303,9 +368,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
self.textPlaceholderNode.maximumNumberOfLines = 1
|
self.textPlaceholderNode.maximumNumberOfLines = 1
|
||||||
self.textPlaceholderNode.isUserInteractionEnabled = false
|
self.textPlaceholderNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.oneLineNode = ImmediateTextNode()
|
self.oneLineNode = TextNodeWithEntities()
|
||||||
self.oneLineNode.maximumNumberOfLines = 1
|
self.oneLineNode.textNode.isUserInteractionEnabled = false
|
||||||
self.oneLineNode.isUserInteractionEnabled = false
|
|
||||||
|
|
||||||
self.actionButtons = AttachmentTextInputActionButtonsNode(presentationInterfaceState: presentationInterfaceState, presentController: presentController)
|
self.actionButtons = AttachmentTextInputActionButtonsNode(presentationInterfaceState: presentationInterfaceState, presentController: presentController)
|
||||||
self.counterTextNode = ImmediateTextNode()
|
self.counterTextNode = ImmediateTextNode()
|
||||||
@ -331,7 +395,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
self.addSubnode(self.counterTextNode)
|
self.addSubnode(self.counterTextNode)
|
||||||
|
|
||||||
if isCaption {
|
if isCaption {
|
||||||
self.addSubnode(self.oneLineNode)
|
self.addSubnode(self.oneLineNode.textNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.textInputBackgroundImageNode.clipsToBounds = true
|
self.textInputBackgroundImageNode.clipsToBounds = true
|
||||||
@ -343,13 +407,13 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
}
|
}
|
||||||
self.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
|
self.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
|
||||||
|
|
||||||
/*self.emojiViewProvider = { [weak self] emoji in
|
self.emojiViewProvider = { [weak self] emoji in
|
||||||
guard let strongSelf = self, let file = strongSelf.context.animatedEmojiStickers[emoji]?.first?.file else {
|
guard let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState else {
|
||||||
return UIView()
|
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)
|
self.updateSendButtonEnabled(isCaption || isAttachment, animated: false)
|
||||||
|
|
||||||
@ -464,11 +528,6 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
textInputNode.view.addGestureRecognizer(recognizer)
|
textInputNode.view.addGestureRecognizer(recognizer)
|
||||||
|
|
||||||
textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string
|
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 {
|
private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat {
|
||||||
@ -528,6 +587,14 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
return minimalHeight
|
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 {
|
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 hadLayout = self.validLayout != nil
|
||||||
let previousAdditionalSideInsets = self.validLayout?.3
|
let previousAdditionalSideInsets = self.validLayout?.3
|
||||||
@ -688,7 +755,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
|
|
||||||
if self.isCaption {
|
if self.isCaption {
|
||||||
if self.isFocused {
|
if self.isFocused {
|
||||||
self.oneLineNode.alpha = 0.0
|
self.oneLineNode.textNode.alpha = 0.0
|
||||||
self.oneLineDustNode?.alpha = 0.0
|
self.oneLineDustNode?.alpha = 0.0
|
||||||
self.textInputNode?.alpha = 1.0
|
self.textInputNode?.alpha = 1.0
|
||||||
|
|
||||||
@ -698,7 +765,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
} else {
|
} else {
|
||||||
panelHeight = minimalHeight
|
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 {
|
if let oneLineDustNode = self.oneLineDustNode {
|
||||||
transition.updateAlpha(node: oneLineDustNode, alpha: inputHasText ? 1.0 : 0.0)
|
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)
|
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 makeOneLineLayout = TextNodeWithEntities.asyncLayout(self.oneLineNode)
|
||||||
let oneLineFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: oneLineSize)
|
let (oneLineLayout, oneLineApply) = makeOneLineLayout(TextNodeLayoutArguments(
|
||||||
self.oneLineNode.frame = oneLineFrame
|
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()
|
self.updateOneLineSpoiler()
|
||||||
}
|
}
|
||||||
@ -725,6 +817,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
if let textInputNode = self.textInputNode {
|
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 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
|
let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size
|
||||||
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||||
|
textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState)
|
||||||
|
}
|
||||||
transition.updateFrame(node: textInputNode, frame: textFieldFrame)
|
transition.updateFrame(node: textInputNode, frame: textFieldFrame)
|
||||||
if shouldUpdateLayout {
|
if shouldUpdateLayout {
|
||||||
textInputNode.layout()
|
textInputNode.layout()
|
||||||
@ -788,6 +883,37 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
var textInputViewRealInsets = UIEdgeInsets()
|
var textInputViewRealInsets = UIEdgeInsets()
|
||||||
if let presentationInterfaceState = self.presentationInterfaceState {
|
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||||
textInputViewRealInsets = calculateTextFieldRealInsets(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
|
let placeholderFrame: CGRect
|
||||||
@ -836,6 +962,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
||||||
|
|
||||||
var rects: [CGRect] = []
|
var rects: [CGRect] = []
|
||||||
|
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
|
||||||
|
|
||||||
if let attributedText = textInputNode.attributedText {
|
if let attributedText = textInputNode.attributedText {
|
||||||
let beginning = textInputNode.textView.beginningOfDocument
|
let beginning = textInputNode.textView.beginningOfDocument
|
||||||
@ -873,6 +1000,16 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
|
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()
|
dustNode.removeFromSupernode()
|
||||||
self.dustNode = nil
|
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) {
|
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) {
|
private func updateTextNodeText(animated: Bool) {
|
||||||
var inputHasText = false
|
var inputHasText = false
|
||||||
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
|
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)))
|
let trimmedText = NSMutableAttributedString(attributedString: attributedText.attributedSubstring(from: NSMakeRange(0, range.location)))
|
||||||
trimmedText.append(NSAttributedString(string: "\u{2026}", font: textFont, textColor: textColor))
|
trimmedText.append(NSAttributedString(string: "\u{2026}", font: textFont, textColor: textColor))
|
||||||
|
|
||||||
self.oneLineNode.attributedText = trimmedText
|
self.oneLineNodeAttributedText = trimmedText
|
||||||
} else {
|
} else {
|
||||||
self.oneLineNode.attributedText = attributedText
|
self.oneLineNodeAttributedText = attributedText
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.oneLineNode.attributedText = nil
|
self.oneLineNodeAttributedText = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let panelHeight = self.updateTextHeight(animated: animated)
|
let panelHeight = self.updateTextHeight(animated: animated)
|
||||||
@ -1052,15 +1276,15 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateOneLineSpoiler() {
|
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 {
|
if self.oneLineDustNode == nil {
|
||||||
let oneLineDustNode = InvisibleInkDustNode(textNode: nil)
|
let oneLineDustNode = InvisibleInkDustNode(textNode: nil)
|
||||||
self.oneLineDustNode = oneLineDustNode
|
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 {
|
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.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
|
oneLineDustNode.frame = textFrame
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import AccountContext
|
|||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
import UIKitRuntimeUtils
|
import UIKitRuntimeUtils
|
||||||
import MediaResources
|
import MediaResources
|
||||||
|
import AttachmentTextInputPanelNode
|
||||||
|
|
||||||
public enum AttachmentButtonType: Equatable {
|
public enum AttachmentButtonType: Equatable {
|
||||||
case gallery
|
case gallery
|
||||||
@ -167,6 +168,7 @@ public class AttachmentController: ViewController {
|
|||||||
private let buttons: [AttachmentButtonType]
|
private let buttons: [AttachmentButtonType]
|
||||||
private let initialButton: AttachmentButtonType
|
private let initialButton: AttachmentButtonType
|
||||||
private let fromMenu: Bool
|
private let fromMenu: Bool
|
||||||
|
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
|
||||||
|
|
||||||
public var willDismiss: () -> Void = {}
|
public var willDismiss: () -> Void = {}
|
||||||
public var didDismiss: () -> Void = {}
|
public var didDismiss: () -> Void = {}
|
||||||
@ -190,6 +192,7 @@ public class AttachmentController: ViewController {
|
|||||||
private let dim: ASDisplayNode
|
private let dim: ASDisplayNode
|
||||||
private let shadowNode: ASImageNode
|
private let shadowNode: ASImageNode
|
||||||
private let container: AttachmentContainer
|
private let container: AttachmentContainer
|
||||||
|
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
|
||||||
let panel: AttachmentPanel
|
let panel: AttachmentPanel
|
||||||
|
|
||||||
private var currentType: AttachmentButtonType?
|
private var currentType: AttachmentButtonType?
|
||||||
@ -259,8 +262,9 @@ public class AttachmentController: ViewController {
|
|||||||
|
|
||||||
private let wrapperNode: ASDisplayNode
|
private let wrapperNode: ASDisplayNode
|
||||||
|
|
||||||
init(controller: AttachmentController) {
|
init(controller: AttachmentController, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
|
self.makeEntityInputView = makeEntityInputView
|
||||||
|
|
||||||
self.dim = ASDisplayNode()
|
self.dim = ASDisplayNode()
|
||||||
self.dim.alpha = 0.0
|
self.dim.alpha = 0.0
|
||||||
@ -274,7 +278,7 @@ public class AttachmentController: ViewController {
|
|||||||
|
|
||||||
self.container = AttachmentContainer()
|
self.container = AttachmentContainer()
|
||||||
self.container.canHaveKeyboardFocus = true
|
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.fromMenu = controller.fromMenu
|
||||||
self.panel.isStandalone = controller.isStandalone
|
self.panel.isStandalone = controller.isStandalone
|
||||||
|
|
||||||
@ -843,13 +847,14 @@ public class AttachmentController: ViewController {
|
|||||||
|
|
||||||
public var getInputContainerNode: () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil }
|
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.context = context
|
||||||
self.updatedPresentationData = updatedPresentationData
|
self.updatedPresentationData = updatedPresentationData
|
||||||
self.chatLocation = chatLocation
|
self.chatLocation = chatLocation
|
||||||
self.buttons = buttons
|
self.buttons = buttons
|
||||||
self.initialButton = initialButton
|
self.initialButton = initialButton
|
||||||
self.fromMenu = fromMenu
|
self.fromMenu = fromMenu
|
||||||
|
self.makeEntityInputView = makeEntityInputView
|
||||||
|
|
||||||
super.init(navigationBarPresentationData: nil)
|
super.init(navigationBarPresentationData: nil)
|
||||||
|
|
||||||
@ -877,7 +882,7 @@ public class AttachmentController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open override func loadDisplayNode() {
|
open override func loadDisplayNode() {
|
||||||
self.displayNode = Node(controller: self)
|
self.displayNode = Node(controller: self, makeEntityInputView: self.makeEntityInputView)
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -458,6 +458,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
private var presentationInterfaceState: ChatPresentationInterfaceState
|
private var presentationInterfaceState: ChatPresentationInterfaceState
|
||||||
private var interfaceInteraction: ChatPanelInterfaceInteraction?
|
private var interfaceInteraction: ChatPanelInterfaceInteraction?
|
||||||
|
|
||||||
|
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
|
||||||
|
|
||||||
private let containerNode: ASDisplayNode
|
private let containerNode: ASDisplayNode
|
||||||
private let backgroundNode: NavigationBackgroundNode
|
private let backgroundNode: NavigationBackgroundNode
|
||||||
private let scrollNode: ASScrollNode
|
private let scrollNode: ASScrollNode
|
||||||
@ -496,9 +498,11 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
|
|
||||||
var mainButtonPressed: () -> Void = { }
|
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.context = context
|
||||||
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
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)
|
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 {
|
if let strongSelf = self {
|
||||||
strongSelf.present(c)
|
strongSelf.present(c)
|
||||||
}
|
}
|
||||||
})
|
}, makeEntityInputView: self.makeEntityInputView)
|
||||||
textInputPanelNode.interfaceInteraction = self.interfaceInteraction
|
textInputPanelNode.interfaceInteraction = self.interfaceInteraction
|
||||||
textInputPanelNode.sendMessage = { [weak self] mode in
|
textInputPanelNode.sendMessage = { [weak self] mode in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
|
|||||||
@ -543,7 +543,11 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
|||||||
case .hide:
|
case .hide:
|
||||||
effectiveTopPanelHeight = 0.0
|
effectiveTopPanelHeight = 0.0
|
||||||
case .show, .hideOnScroll:
|
case .show, .hideOnScroll:
|
||||||
effectiveTopPanelHeight = topPanelHeight
|
if component.externalTopPanelContainer != nil {
|
||||||
|
effectiveTopPanelHeight = topPanelHeight
|
||||||
|
} else {
|
||||||
|
effectiveTopPanelHeight = 0.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let contentBackground = component.contentBackground {
|
if let contentBackground = component.contentBackground {
|
||||||
|
|||||||
@ -41,6 +41,9 @@ swift_library(
|
|||||||
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
|
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
|
||||||
"//submodules/TranslateUI:TranslateUI",
|
"//submodules/TranslateUI:TranslateUI",
|
||||||
"//submodules/Utils/RangeSet:RangeSet",
|
"//submodules/Utils/RangeSet:RangeSet",
|
||||||
|
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
|
||||||
|
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||||
|
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -22,6 +22,9 @@ import UndoUI
|
|||||||
import ManagedAnimationNode
|
import ManagedAnimationNode
|
||||||
import TelegramUniversalVideoContent
|
import TelegramUniversalVideoContent
|
||||||
import InvisibleInkDustNode
|
import InvisibleInkDustNode
|
||||||
|
import TextNodeWithEntities
|
||||||
|
import AnimationCache
|
||||||
|
import MultiAnimationRenderer
|
||||||
|
|
||||||
private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white)
|
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)
|
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 scrollWrapperNode: CaptionScrollWrapperNode
|
||||||
private let scrollNode: ASScrollNode
|
private let scrollNode: ASScrollNode
|
||||||
|
|
||||||
private let textNode: ImmediateTextNode
|
private let textNode: ImmediateTextNodeWithEntities
|
||||||
private var spoilerTextNode: ImmediateTextNode?
|
private var spoilerTextNode: ImmediateTextNodeWithEntities?
|
||||||
private var dustNode: InvisibleInkDustNode?
|
private var dustNode: InvisibleInkDustNode?
|
||||||
|
|
||||||
|
private let animationCache: AnimationCache
|
||||||
|
private let animationRenderer: MultiAnimationRenderer
|
||||||
|
|
||||||
private let authorNameNode: ASTextNode
|
private let authorNameNode: ASTextNode
|
||||||
private let dateNode: ASTextNode
|
private let dateNode: ASTextNode
|
||||||
private let backwardButton: PlaybackButtonNode
|
private let backwardButton: PlaybackButtonNode
|
||||||
@ -319,7 +325,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
|||||||
|
|
||||||
self.maskNode = ASDisplayNode()
|
self.maskNode = ASDisplayNode()
|
||||||
|
|
||||||
self.textNode = ImmediateTextNode()
|
self.textNode = ImmediateTextNodeWithEntities()
|
||||||
self.textNode.maximumNumberOfLines = 0
|
self.textNode.maximumNumberOfLines = 0
|
||||||
self.textNode.linkHighlightColor = UIColor(rgb: 0x5ac8fa, alpha: 0.2)
|
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 = RadialStatusNode(backgroundNodeColor: .clear)
|
||||||
self.statusNode.isUserInteractionEnabled = false
|
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()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.contentNode)
|
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.deleteButton)
|
||||||
self.contentNode.view.addSubview(self.fullscreenButton)
|
self.contentNode.view.addSubview(self.fullscreenButton)
|
||||||
self.contentNode.view.addSubview(self.actionButton)
|
self.contentNode.view.addSubview(self.actionButton)
|
||||||
@ -723,7 +743,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
|||||||
private func updateSpoilers(textFrame: CGRect) {
|
private func updateSpoilers(textFrame: CGRect) {
|
||||||
if let textLayout = self.textNode.cachedLayout, !textLayout.spoilers.isEmpty {
|
if let textLayout = self.textNode.cachedLayout, !textLayout.spoilers.isEmpty {
|
||||||
if self.spoilerTextNode == nil {
|
if self.spoilerTextNode == nil {
|
||||||
let spoilerTextNode = ImmediateTextNode()
|
let spoilerTextNode = ImmediateTextNodeWithEntities()
|
||||||
spoilerTextNode.attributedText = textNode.attributedText
|
spoilerTextNode.attributedText = textNode.attributedText
|
||||||
spoilerTextNode.maximumNumberOfLines = 0
|
spoilerTextNode.maximumNumberOfLines = 0
|
||||||
spoilerTextNode.linkHighlightColor = UIColor(rgb: 0x5ac8fa, alpha: 0.2)
|
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 hideInputUpdated: (Bool, Bool, Transition) -> Void
|
||||||
public let switchToTextInput: () -> Void
|
public let switchToTextInput: () -> Void
|
||||||
public let switchToGifSubject: (GifPagerContentComponent.Subject) -> Void
|
public let switchToGifSubject: (GifPagerContentComponent.Subject) -> Void
|
||||||
public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode
|
public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode?
|
||||||
public let deviceMetrics: DeviceMetrics
|
public let deviceMetrics: DeviceMetrics
|
||||||
public let hiddenInputHeight: CGFloat
|
public let hiddenInputHeight: CGFloat
|
||||||
public let isExpanded: Bool
|
public let isExpanded: Bool
|
||||||
@ -103,7 +103,7 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void,
|
hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void,
|
||||||
switchToTextInput: @escaping () -> Void,
|
switchToTextInput: @escaping () -> Void,
|
||||||
switchToGifSubject: @escaping (GifPagerContentComponent.Subject) -> Void,
|
switchToGifSubject: @escaping (GifPagerContentComponent.Subject) -> Void,
|
||||||
makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode,
|
makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode?,
|
||||||
deviceMetrics: DeviceMetrics,
|
deviceMetrics: DeviceMetrics,
|
||||||
hiddenInputHeight: CGFloat,
|
hiddenInputHeight: CGFloat,
|
||||||
isExpanded: Bool
|
isExpanded: Bool
|
||||||
@ -486,7 +486,8 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
)),
|
)),
|
||||||
topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent(
|
topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent(
|
||||||
theme: component.theme,
|
theme: component.theme,
|
||||||
overflowHeight: component.hiddenInputHeight
|
overflowHeight: component.hiddenInputHeight,
|
||||||
|
displayBackground: component.externalTopPanelContainer == nil
|
||||||
)),
|
)),
|
||||||
externalTopPanelContainer: component.externalTopPanelContainer,
|
externalTopPanelContainer: component.externalTopPanelContainer,
|
||||||
bottomPanel: AnyComponent(EntityKeyboardBottomPanelComponent(
|
bottomPanel: AnyComponent(EntityKeyboardBottomPanelComponent(
|
||||||
|
|||||||
@ -278,59 +278,64 @@ final class EntityKeyboardBottomPanelComponent: Component {
|
|||||||
|
|
||||||
let navigateToContentId = panelEnvironment.navigateToContentId
|
let navigateToContentId = panelEnvironment.navigateToContentId
|
||||||
|
|
||||||
for icon in panelEnvironment.contentIcons {
|
if panelEnvironment.contentIcons.count > 1 {
|
||||||
validIconIds.append(icon.id)
|
for icon in panelEnvironment.contentIcons {
|
||||||
|
validIconIds.append(icon.id)
|
||||||
var iconTransition = transition
|
|
||||||
let iconView: ComponentHostView<Empty>
|
var iconTransition = transition
|
||||||
if let current = self.iconViews[icon.id] {
|
let iconView: ComponentHostView<Empty>
|
||||||
iconView = current
|
if let current = self.iconViews[icon.id] {
|
||||||
} else {
|
iconView = current
|
||||||
iconTransition = .immediate
|
} else {
|
||||||
iconView = ComponentHostView<Empty>()
|
iconTransition = .immediate
|
||||||
self.iconViews[icon.id] = iconView
|
iconView = ComponentHostView<Empty>()
|
||||||
self.addSubview(iconView)
|
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))
|
var nextIconOrigin = CGPoint(x: floor((availableSize.width - iconTotalSize.width) / 2.0), y: floor((intrinsicHeight - iconTotalSize.height) / 2.0))
|
||||||
if component.bottomInset > 0.0 {
|
if component.bottomInset > 0.0 {
|
||||||
nextIconOrigin.y += 2.0
|
nextIconOrigin.y += 2.0
|
||||||
}
|
}
|
||||||
for icon in panelEnvironment.contentIcons {
|
|
||||||
guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else {
|
if panelEnvironment.contentIcons.count > 1 {
|
||||||
continue
|
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 {
|
if activeContentId == nil {
|
||||||
|
|||||||
@ -32,13 +32,16 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
|||||||
|
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let overflowHeight: CGFloat
|
let overflowHeight: CGFloat
|
||||||
|
let displayBackground: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
theme: PresentationTheme,
|
theme: PresentationTheme,
|
||||||
overflowHeight: CGFloat
|
overflowHeight: CGFloat,
|
||||||
|
displayBackground: Bool
|
||||||
) {
|
) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.overflowHeight = overflowHeight
|
self.overflowHeight = overflowHeight
|
||||||
|
self.displayBackground = displayBackground
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: EntityKeyboardTopContainerPanelComponent, rhs: EntityKeyboardTopContainerPanelComponent) -> Bool {
|
static func ==(lhs: EntityKeyboardTopContainerPanelComponent, rhs: EntityKeyboardTopContainerPanelComponent) -> Bool {
|
||||||
@ -48,6 +51,9 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
|||||||
if lhs.overflowHeight != rhs.overflowHeight {
|
if lhs.overflowHeight != rhs.overflowHeight {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.displayBackground != rhs.displayBackground {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -59,6 +65,9 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class View: UIView {
|
final class View: UIView {
|
||||||
|
private var backgroundView: BlurredBackgroundView?
|
||||||
|
private var backgroundSeparatorView: UIView?
|
||||||
|
|
||||||
private var panelViews: [AnyHashable: PanelView] = [:]
|
private var panelViews: [AnyHashable: PanelView] = [:]
|
||||||
|
|
||||||
private var component: EntityKeyboardTopContainerPanelComponent?
|
private var component: EntityKeyboardTopContainerPanelComponent?
|
||||||
@ -183,6 +192,40 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
|||||||
strongSelf.updateVisibilityFraction(value: fraction, transition: transition)
|
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)
|
return CGSize(width: availableSize.width, height: height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,11 +51,11 @@ final class EntitySearchContentEnvironment: Equatable {
|
|||||||
final class EntitySearchContentComponent: Component {
|
final class EntitySearchContentComponent: Component {
|
||||||
typealias EnvironmentType = EntitySearchContentEnvironment
|
typealias EnvironmentType = EntitySearchContentEnvironment
|
||||||
|
|
||||||
let makeContainerNode: () -> EntitySearchContainerNode
|
let makeContainerNode: () -> EntitySearchContainerNode?
|
||||||
let dismissSearch: () -> Void
|
let dismissSearch: () -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
makeContainerNode: @escaping () -> EntitySearchContainerNode,
|
makeContainerNode: @escaping () -> EntitySearchContainerNode?,
|
||||||
dismissSearch: @escaping () -> Void
|
dismissSearch: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.makeContainerNode = makeContainerNode
|
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 {
|
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 {
|
if let current = self.containerNode {
|
||||||
containerNode = current
|
containerNode = current
|
||||||
} else {
|
} else {
|
||||||
containerNode = component.makeContainerNode()
|
containerNode = component.makeContainerNode()
|
||||||
self.containerNode = containerNode
|
if let containerNode = containerNode {
|
||||||
self.addSubnode(containerNode)
|
self.containerNode = containerNode
|
||||||
|
self.addSubnode(containerNode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let containerNode = containerNode {
|
||||||
|
|
||||||
let environmentValue = environment[EntitySearchContentEnvironment.self].value
|
let environmentValue = environment[EntitySearchContentEnvironment.self].value
|
||||||
|
transition.setFrame(view: containerNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||||
transition.setFrame(view: containerNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
|
containerNode.updateLayout(
|
||||||
containerNode.updateLayout(
|
size: availableSize,
|
||||||
size: availableSize,
|
leftInset: 0.0,
|
||||||
leftInset: 0.0,
|
rightInset: 0.0,
|
||||||
rightInset: 0.0,
|
bottomInset: 0.0,
|
||||||
bottomInset: 0.0,
|
inputHeight: 0.0,
|
||||||
inputHeight: 0.0,
|
deviceMetrics: environmentValue.deviceMetrics,
|
||||||
deviceMetrics: environmentValue.deviceMetrics,
|
transition: transition.containedViewLayoutTransition
|
||||||
transition: transition.containedViewLayoutTransition
|
)
|
||||||
)
|
|
||||||
|
containerNode.onCancel = {
|
||||||
containerNode.onCancel = {
|
component.dismissSearch()
|
||||||
component.dismissSearch()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
|
|||||||
@ -118,7 +118,7 @@ public final class TextNodeWithEntities {
|
|||||||
if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont {
|
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)
|
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(
|
let runDelegateData = RunDelegateData(
|
||||||
ascent: font.ascender,
|
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.interfaceInteraction = interfaceInteraction
|
||||||
inputPanelNode.effectivePresentationInterfaceState = {
|
inputPanelNode.effectivePresentationInterfaceState = {
|
||||||
return presentationInterfaceState
|
return presentationInterfaceState
|
||||||
@ -10967,7 +10973,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
let currentFilesController = Atomic<AttachmentContainable?>(value: nil)
|
let currentFilesController = Atomic<AttachmentContainable?>(value: nil)
|
||||||
let currentLocationController = 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
|
attachmentController.requestController = { [weak self, weak attachmentController] type, completion in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -540,14 +540,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.addSubnode(self.inputPanelContainerNode)
|
self.addSubnode(self.inputPanelContainerNode)
|
||||||
|
self.addSubnode(self.inputContextPanelContainer)
|
||||||
|
|
||||||
self.inputPanelContainerNode.addSubnode(self.inputPanelClippingNode)
|
self.inputPanelContainerNode.addSubnode(self.inputPanelClippingNode)
|
||||||
self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundNode)
|
self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundNode)
|
||||||
self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundSeparatorNode)
|
self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundSeparatorNode)
|
||||||
self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode)
|
self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode)
|
||||||
|
|
||||||
self.contentContainerNode.addSubnode(self.inputContextPanelContainer)
|
|
||||||
|
|
||||||
self.addSubnode(self.messageTransitionNode)
|
self.addSubnode(self.messageTransitionNode)
|
||||||
self.contentContainerNode.addSubnode(self.navigateButtons)
|
self.contentContainerNode.addSubnode(self.navigateButtons)
|
||||||
self.contentContainerNode.addSubnode(self.presentationContextMarker)
|
self.contentContainerNode.addSubnode(self.presentationContextMarker)
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import AudioToolbox
|
|||||||
import UndoUI
|
import UndoUI
|
||||||
import ContextUI
|
import ContextUI
|
||||||
import GalleryUI
|
import GalleryUI
|
||||||
|
import AttachmentTextInputPanelNode
|
||||||
|
import TelegramPresentationData
|
||||||
|
|
||||||
private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = {
|
private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = {
|
||||||
guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else {
|
guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else {
|
||||||
@ -46,14 +48,14 @@ private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment,
|
|||||||
final class ChatEntityKeyboardInputNode: ChatInputNode {
|
final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||||
struct InputData: Equatable {
|
struct InputData: Equatable {
|
||||||
var emoji: EmojiPagerContentComponent
|
var emoji: EmojiPagerContentComponent
|
||||||
var stickers: EmojiPagerContentComponent
|
var stickers: EmojiPagerContentComponent?
|
||||||
var gifs: GifPagerContentComponent
|
var gifs: GifPagerContentComponent?
|
||||||
var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
|
var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
|
||||||
|
|
||||||
init(
|
init(
|
||||||
emoji: EmojiPagerContentComponent,
|
emoji: EmojiPagerContentComponent,
|
||||||
stickers: EmojiPagerContentComponent,
|
stickers: EmojiPagerContentComponent?,
|
||||||
gifs: GifPagerContentComponent,
|
gifs: GifPagerContentComponent?,
|
||||||
availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
|
availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
|
||||||
) {
|
) {
|
||||||
self.emoji = emoji
|
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 premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||||
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
||||||
|
|
||||||
@ -177,9 +281,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
chatPeerId: chatPeerId
|
chatPeerId: chatPeerId
|
||||||
)
|
)
|
||||||
let stickerInputInteraction = EmojiPagerContentComponent.InputInteraction(
|
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
|
let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
|
||||||
guard let interfaceInteraction = interfaceInteraction else {
|
guard let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let file = item.file {
|
if let file = item.file {
|
||||||
@ -249,98 +353,13 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
animationRenderer = MultiAnimationRendererImpl()
|
animationRenderer = MultiAnimationRendererImpl()
|
||||||
//}
|
//}
|
||||||
|
|
||||||
let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
|
let emojiItems = emojiInputData(context: context, inputInteraction: emojiInputInteraction, animationCache: animationCache, animationRenderer: animationRenderer)
|
||||||
let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
|
|
||||||
|
|
||||||
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
|
let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
|
||||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
|
||||||
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 stickerItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
|
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
|
hasPremium
|
||||||
)
|
)
|
||||||
|> map { view, hasPremium -> EmojiPagerContentComponent in
|
|> map { view, hasPremium -> EmojiPagerContentComponent in
|
||||||
@ -604,7 +623,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
private var currentInputData: InputData
|
private var currentInputData: InputData
|
||||||
private var inputDataDisposable: Disposable?
|
private var inputDataDisposable: Disposable?
|
||||||
|
|
||||||
private let controllerInteraction: ChatControllerInteraction
|
private let controllerInteraction: ChatControllerInteraction?
|
||||||
|
|
||||||
private var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
private var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||||
|
|
||||||
@ -617,6 +636,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
return self.externalTopPanelContainerImpl
|
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 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 {
|
private var gifMode: GifPagerContentComponent.Subject = .recent {
|
||||||
@ -811,7 +832,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
private let gifComponent = Promise<GifPagerContentComponent>()
|
private let gifComponent = Promise<GifPagerContentComponent>()
|
||||||
private var gifInputInteraction: GifPagerContentComponent.InputInteraction?
|
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.context = context
|
||||||
self.currentInputData = currentInputData
|
self.currentInputData = currentInputData
|
||||||
self.defaultToEmojiTab = defaultToEmojiTab
|
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()
|
self.reloadGifContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -996,12 +1026,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
switchToTextInput: { [weak self] in
|
switchToTextInput: { [weak self] in
|
||||||
guard let strongSelf = self else {
|
self?.switchToTextInput?()
|
||||||
return
|
|
||||||
}
|
|
||||||
strongSelf.controllerInteraction.updateInputMode { _ in
|
|
||||||
return .text
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
switchToGifSubject: { [weak self] subject in
|
switchToGifSubject: { [weak self] subject in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -1009,7 +1034,11 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
}
|
}
|
||||||
strongSelf.gifMode = subject
|
strongSelf.gifMode = subject
|
||||||
},
|
},
|
||||||
makeSearchContainerNode: { content in
|
makeSearchContainerNode: { [weak controllerInteraction] content in
|
||||||
|
guard let controllerInteraction = controllerInteraction else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
let mappedMode: ChatMediaInputSearchMode
|
let mappedMode: ChatMediaInputSearchMode
|
||||||
switch content {
|
switch content {
|
||||||
case .stickers:
|
case .stickers:
|
||||||
@ -1074,7 +1103,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
}, action: { _, f in
|
}, action: { _, f in
|
||||||
f(.default)
|
f(.default)
|
||||||
if isSaved {
|
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 {
|
}/* else if let (collection, result) = file.contextResult {
|
||||||
let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false)
|
let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false)
|
||||||
}*/
|
}*/
|
||||||
@ -1141,7 +1170,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
|> deliverOnMainQueue).start(next: { result in
|
|> deliverOnMainQueue).start(next: { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .generic:
|
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):
|
case let .limitExceeded(limit, premiumLimit):
|
||||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||||
let text: String
|
let text: String
|
||||||
@ -1150,10 +1179,10 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
} else {
|
} else {
|
||||||
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string
|
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 {
|
if case .info = action {
|
||||||
let controller = PremiumIntroScreen(context: context, source: .savedGifs)
|
let controller = PremiumIntroScreen(context: context, source: .savedGifs)
|
||||||
controllerInteraction.navigationController()?.pushViewController(controller)
|
controllerInteraction?.navigationController()?.pushViewController(controller)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
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)
|
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.addSubnode(forwardAccessoryPanelNode)
|
||||||
self.forwardAccessoryPanelNode = 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.interfaceInteraction = self.interfaceInteraction
|
||||||
textInputPanelNode.sendMessage = { [weak self] mode in
|
textInputPanelNode.sendMessage = { [weak self] mode in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
|
|||||||
@ -70,6 +70,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
|||||||
self.textNode.maximumNumberOfLines = 1
|
self.textNode.maximumNumberOfLines = 1
|
||||||
self.textNode.displaysAsynchronously = false
|
self.textNode.displaysAsynchronously = false
|
||||||
self.textNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
|
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 {
|
if let animationCache = animationCache, let animationRenderer = animationRenderer {
|
||||||
self.textNode.arguments = TextNodeWithEntities.Arguments(
|
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 {
|
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.getInputContainerNode = getInputContainerNode
|
||||||
controller.requestController = { _, present in
|
controller.requestController = { _, present in
|
||||||
let webAppController = WebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil)
|
let webAppController = WebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user