Emoji input and display in media selection

This commit is contained in:
Ali 2022-07-15 14:49:48 +02:00
parent 0577baac79
commit eaf0b74f1b
18 changed files with 829 additions and 250 deletions

View File

@ -29,6 +29,11 @@ swift_library(
"//submodules/Pasteboard:Pasteboard",
"//submodules/ContextUI:ContextUI",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
],
visibility = [
"//visibility:public",

View File

@ -19,6 +19,11 @@ import TextInputMenu
import ChatPresentationInterfaceState
import Pasteboard
import EmojiTextAttachmentView
import ComponentFlow
import LottieAnimationComponent
import AnimationCache
import MultiAnimationRenderer
import TextNodeWithEntities
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
private let minInputFontSize: CGFloat = 5.0
@ -64,7 +69,7 @@ private func calculateTextFieldRealInsets(_ presentationInterfaceState: ChatPres
top = 0.0
bottom = 0.0
}
return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: 0.0)
return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: 32.0)
}
private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, UIImage)?
@ -108,33 +113,6 @@ private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackground
}
}
private final class EntityInputView: UIInputView, UIInputViewAudioFeedback {
override var inputViewStyle: UIInputView.Style {
get {
return .default
}
}
override var allowsSelfSizing: Bool {
get {
return true
}
set {
}
}
override var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: 300)
}
override func sizeToFit() {
print("sizeToFit")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: size.width, height: 100.0)
}
}
private class CaptionEditableTextNode: EditableTextNode {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let previousAlpha = self.alpha
@ -145,18 +123,91 @@ private class CaptionEditableTextNode: EditableTextNode {
}
}
public protocol AttachmentTextInputPanelInputView: UIView {
var insertText: ((NSAttributedString) -> Void)? { get set }
var deleteBackwards: (() -> Void)? { get set }
var switchToKeyboard: (() -> Void)? { get set }
var presentController: ((ViewController) -> Void)? { get set }
}
final class CustomEmojiContainerView: UIView {
private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView?
private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:]
init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) {
self.emojiViewProvider = emojiViewProvider
super.init(frame: CGRect())
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) {
var nextIndexById: [Int64: Int] = [:]
var validKeys = Set<InlineStickerItemLayer.Key>()
for (rect, emoji) in emojiRects {
let index: Int
if let nextIndex = nextIndexById[emoji.fileId] {
index = nextIndex
} else {
index = 0
}
nextIndexById[emoji.fileId] = index + 1
let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index)
let view: UIView
if let current = self.emojiLayers[key] {
view = current
} else if let newView = self.emojiViewProvider(emoji) {
view = newView
self.addSubview(newView)
self.emojiLayers[key] = view
} else {
continue
}
let size = CGSize(width: 24.0, height: 24.0)
view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0)), size: size)
validKeys.insert(key)
}
var removeKeys: [InlineStickerItemLayer.Key] = []
for (key, view) in self.emojiLayers {
if !validKeys.contains(key) {
removeKeys.append(key)
view.removeFromSuperview()
}
}
for key in removeKeys {
self.emojiLayers.removeValue(forKey: key)
}
}
}
public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, ASEditableTextNodeDelegate {
private let context: AccountContext
private let isCaption: Bool
private let isAttachment: Bool
private let presentController: (ViewController) -> Void
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
private var textPlaceholderNode: ImmediateTextNode
private let textInputContainerBackgroundNode: ASImageNode
private let textInputContainer: ASDisplayNode
public var textInputNode: EditableTextNode?
private var dustNode: InvisibleInkDustNode?
private var oneLineNode: ImmediateTextNode
private var customEmojiContainerView: CustomEmojiContainerView?
private var oneLineNode: TextNodeWithEntities
private var oneLineNodeAttributedText: NSAttributedString?
private var oneLineDustNode: InvisibleInkDustNode?
let textInputBackgroundNode: ASDisplayNode
@ -164,6 +215,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
private var transparentTextInputBackgroundImage: UIImage?
private let actionButtons: AttachmentTextInputActionButtonsNode
private let counterTextNode: ImmediateTextNode
private let inputModeView: ComponentHostView<Empty>
private var validLayout: (CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool)?
@ -270,14 +323,23 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
private var spoilersRevealed = false
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private var maxCaptionLength: Int32?
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, presentController: @escaping (ViewController) -> Void) {
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, presentController: @escaping (ViewController) -> Void, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
self.context = context
self.presentationInterfaceState = presentationInterfaceState
self.isCaption = isCaption
self.isAttachment = isAttachment
self.presentController = presentController
self.makeEntityInputView = makeEntityInputView
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
var hasSpoilers = true
if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
@ -293,6 +355,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
if !isCaption {
self.textInputContainer.addSubnode(self.textInputContainerBackgroundNode)
}
self.inputModeView = ComponentHostView<Empty>()
self.textInputContainer.view.addSubview(self.inputModeView)
self.textInputContainer.clipsToBounds = true
self.textInputBackgroundNode = ASDisplayNode()
@ -303,9 +368,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
self.textPlaceholderNode.maximumNumberOfLines = 1
self.textPlaceholderNode.isUserInteractionEnabled = false
self.oneLineNode = ImmediateTextNode()
self.oneLineNode.maximumNumberOfLines = 1
self.oneLineNode.isUserInteractionEnabled = false
self.oneLineNode = TextNodeWithEntities()
self.oneLineNode.textNode.isUserInteractionEnabled = false
self.actionButtons = AttachmentTextInputActionButtonsNode(presentationInterfaceState: presentationInterfaceState, presentController: presentController)
self.counterTextNode = ImmediateTextNode()
@ -331,7 +395,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
self.addSubnode(self.counterTextNode)
if isCaption {
self.addSubnode(self.oneLineNode)
self.addSubnode(self.oneLineNode.textNode)
}
self.textInputBackgroundImageNode.clipsToBounds = true
@ -343,13 +407,13 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
self.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
/*self.emojiViewProvider = { [weak self] emoji in
guard let strongSelf = self, let file = strongSelf.context.animatedEmojiStickers[emoji]?.first?.file else {
self.emojiViewProvider = { [weak self] emoji in
guard let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState else {
return UIView()
}
return EmojiTextAttachmentView(context: context, file: file)
}*/
return EmojiTextAttachmentView(context: context, emoji: emoji, file: emoji.file, cache: strongSelf.animationCache, renderer: strongSelf.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12))
}
self.updateSendButtonEnabled(isCaption || isAttachment, animated: false)
@ -464,11 +528,6 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
textInputNode.view.addGestureRecognizer(recognizer)
textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string
/*let entityInputView = EntityInputView()
entityInputView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 100.0))
entityInputView.backgroundColor = .blue
textInputNode.textView.inputView = entityInputView*/
}
private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat {
@ -528,6 +587,14 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
return minimalHeight
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.inputModeView.isHidden, let result = self.inputModeView.hitTest(self.view.convert(point, to: self.inputModeView), with: event) {
return result
}
return super.hitTest(point, with: event)
}
public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
let hadLayout = self.validLayout != nil
let previousAdditionalSideInsets = self.validLayout?.3
@ -688,7 +755,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
if self.isCaption {
if self.isFocused {
self.oneLineNode.alpha = 0.0
self.oneLineNode.textNode.alpha = 0.0
self.oneLineDustNode?.alpha = 0.0
self.textInputNode?.alpha = 1.0
@ -698,7 +765,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
} else {
panelHeight = minimalHeight
transition.updateAlpha(node: self.oneLineNode, alpha: inputHasText ? 1.0 : 0.0)
transition.updateAlpha(node: self.oneLineNode.textNode, alpha: inputHasText ? 1.0 : 0.0)
if let oneLineDustNode = self.oneLineDustNode {
transition.updateAlpha(node: oneLineDustNode, alpha: inputHasText ? 1.0 : 0.0)
}
@ -711,9 +778,34 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
transition.updateAlpha(node: self.textInputBackgroundImageNode, alpha: inputHasText ? 1.0 : 0.0)
}
let oneLineSize = self.oneLineNode.updateLayout(CGSize(width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: CGFloat.greatestFiniteMagnitude))
let oneLineFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: oneLineSize)
self.oneLineNode.frame = oneLineFrame
let makeOneLineLayout = TextNodeWithEntities.asyncLayout(self.oneLineNode)
let (oneLineLayout, oneLineApply) = makeOneLineLayout(TextNodeLayoutArguments(
attributedString: self.oneLineNodeAttributedText,
backgroundColor: nil,
minimumNumberOfLines: 1,
maximumNumberOfLines: 1,
truncationType: .end,
constrainedSize: CGSize(width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: CGFloat.greatestFiniteMagnitude),
alignment: .left,
verticalAlignment: .top,
lineSpacing: 0.0,
cutout: nil, insets: UIEdgeInsets(),
lineColor: nil,
textShadowColor: nil,
textStroke: nil,
displaySpoilers: false,
displayEmbeddedItemsUnderSpoilers: false
))
let oneLineFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: oneLineLayout.size)
self.oneLineNode.textNode.frame = oneLineFrame
let _ = oneLineApply(TextNodeWithEntities.Arguments(
context: self.context,
cache: self.animationCache,
renderer: self.animationRenderer,
placeholderColor: self.presentationInterfaceState?.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12) ?? .lightGray,
attemptSynchronous: false
))
self.updateOneLineSpoiler()
}
@ -725,6 +817,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
if let textInputNode = self.textInputNode {
let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom))
let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size
if let presentationInterfaceState = self.presentationInterfaceState {
textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState)
}
transition.updateFrame(node: textInputNode, frame: textFieldFrame)
if shouldUpdateLayout {
textInputNode.layout()
@ -788,6 +883,37 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
var textInputViewRealInsets = UIEdgeInsets()
if let presentationInterfaceState = self.presentationInterfaceState {
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState)
var colors: [String: UIColor] = [:]
let colorKeys: [String] = [
"Ellipse 33.Ellipse 33.Stroke 1",
"Ellipse 34.Ellipse 34.Stroke 1",
"Oval.Oval.Fill 1",
"Oval 2.Oval.Fill 1",
"Path 85.Path 85.Stroke 1"
]
for colorKey in colorKeys {
colors[colorKey] = presentationInterfaceState.theme.chat.inputPanel.inputControlColor
}
let animationComponent = LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "anim_smiletosticker",
colors: colors,
mode: .animateTransitionFromPrevious
),
size: CGSize(width: 32.0, height: 32.0)
)
let inputNodeSize = self.inputModeView.update(
transition: .immediate,
component: AnyComponent(Button(
content: AnyComponent(animationComponent),
action: { [weak self] in
self?.toggleInputMode()
})),
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)
transition.updateFrame(view: self.inputModeView, frame: CGRect(origin: CGPoint(x: textInputBackgroundFrame.maxX - inputNodeSize.width - 1.0, y: textInputBackgroundFrame.maxY - inputNodeSize.height - 1.0), size: inputNodeSize))
}
let placeholderFrame: CGRect
@ -836,6 +962,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
var rects: [CGRect] = []
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
if let attributedText = textInputNode.attributedText {
let beginning = textInputNode.textView.beginningOfDocument
@ -873,6 +1000,16 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
}
}
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) {
let textRects = textInputNode.textView.selectionRects(for: textRange)
for textRect in textRects {
customEmojiRects.append((textRect.rect, value))
break
}
}
}
})
}
@ -893,6 +1030,28 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
dustNode.removeFromSupernode()
self.dustNode = nil
}
if !customEmojiRects.isEmpty {
let customEmojiContainerView: CustomEmojiContainerView
if let current = self.customEmojiContainerView {
customEmojiContainerView = current
} else {
customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else {
return nil
}
return emojiViewProvider(emoji)
})
customEmojiContainerView.isUserInteractionEnabled = false
textInputNode.textView.addSubview(customEmojiContainerView)
self.customEmojiContainerView = customEmojiContainerView
}
customEmojiContainerView.update(emojiRects: customEmojiRects)
} else if let customEmojiContainerView = self.customEmojiContainerView {
customEmojiContainerView.removeFromSuperview()
self.customEmojiContainerView = nil
}
}
private func updateSpoilersRevealed(animated: Bool = true) {
@ -1016,6 +1175,71 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
}
private func toggleInputMode() {
self.loadTextInputNodeIfNeeded()
guard let textInputNode = self.textInputNode else {
return
}
var shouldHaveInputView = false
if textInputNode.textView.isFirstResponder {
if textInputNode.textView.inputView == nil {
shouldHaveInputView = true
}
} else {
shouldHaveInputView = true
}
if shouldHaveInputView {
let inputView = self.makeEntityInputView()
inputView?.insertText = { [weak self] text in
guard let strongSelf = self else {
return
}
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { textInputState, inputMode in
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
let range = textInputState.selectionRange
inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: text)
let selectionPosition = range.lowerBound + (text.string as NSString).length
return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode)
}
}
inputView?.deleteBackwards = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.textInputNode?.textView.deleteBackward()
}
inputView?.switchToKeyboard = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.toggleInputMode()
}
inputView?.presentController = { [weak self] c in
guard let strongSelf = self else {
return
}
strongSelf.presentController(c)
}
textInputNode.textView.inputView = inputView
} else {
textInputNode.textView.inputView = nil
}
if textInputNode.textView.isFirstResponder {
textInputNode.textView.reloadInputViews()
} else {
textInputNode.textView.becomeFirstResponder()
}
}
private func updateTextNodeText(animated: Bool) {
var inputHasText = false
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
@ -1037,12 +1261,12 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
let trimmedText = NSMutableAttributedString(attributedString: attributedText.attributedSubstring(from: NSMakeRange(0, range.location)))
trimmedText.append(NSAttributedString(string: "\u{2026}", font: textFont, textColor: textColor))
self.oneLineNode.attributedText = trimmedText
self.oneLineNodeAttributedText = trimmedText
} else {
self.oneLineNode.attributedText = attributedText
self.oneLineNodeAttributedText = attributedText
}
} else {
self.oneLineNode.attributedText = nil
self.oneLineNodeAttributedText = nil
}
let panelHeight = self.updateTextHeight(animated: animated)
@ -1052,15 +1276,15 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
private func updateOneLineSpoiler() {
if let textLayout = self.oneLineNode.cachedLayout, !textLayout.spoilers.isEmpty {
if let textLayout = self.oneLineNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty {
if self.oneLineDustNode == nil {
let oneLineDustNode = InvisibleInkDustNode(textNode: nil)
self.oneLineDustNode = oneLineDustNode
self.oneLineNode.supernode?.insertSubnode(oneLineDustNode, aboveSubnode: self.oneLineNode)
self.oneLineNode.textNode.supernode?.insertSubnode(oneLineDustNode, aboveSubnode: self.oneLineNode.textNode)
}
if let oneLineDustNode = self.oneLineDustNode {
let textFrame = self.oneLineNode.frame.insetBy(dx: 0.0, dy: -3.0)
let textFrame = self.oneLineNode.textNode.frame.insetBy(dx: 0.0, dy: -3.0)
oneLineDustNode.update(size: textFrame.size, color: .white, textColor: .white, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 0.0, dy: 3.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 0.0, dy: 3.0) })
oneLineDustNode.frame = textFrame

View File

@ -11,6 +11,7 @@ import AccountContext
import TelegramStringFormatting
import UIKitRuntimeUtils
import MediaResources
import AttachmentTextInputPanelNode
public enum AttachmentButtonType: Equatable {
case gallery
@ -167,6 +168,7 @@ public class AttachmentController: ViewController {
private let buttons: [AttachmentButtonType]
private let initialButton: AttachmentButtonType
private let fromMenu: Bool
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
public var willDismiss: () -> Void = {}
public var didDismiss: () -> Void = {}
@ -190,6 +192,7 @@ public class AttachmentController: ViewController {
private let dim: ASDisplayNode
private let shadowNode: ASImageNode
private let container: AttachmentContainer
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
let panel: AttachmentPanel
private var currentType: AttachmentButtonType?
@ -259,8 +262,9 @@ public class AttachmentController: ViewController {
private let wrapperNode: ASDisplayNode
init(controller: AttachmentController) {
init(controller: AttachmentController, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
self.controller = controller
self.makeEntityInputView = makeEntityInputView
self.dim = ASDisplayNode()
self.dim.alpha = 0.0
@ -274,7 +278,7 @@ public class AttachmentController: ViewController {
self.container = AttachmentContainer()
self.container.canHaveKeyboardFocus = true
self.panel = AttachmentPanel(context: controller.context, chatLocation: controller.chatLocation, updatedPresentationData: controller.updatedPresentationData)
self.panel = AttachmentPanel(context: controller.context, chatLocation: controller.chatLocation, updatedPresentationData: controller.updatedPresentationData, makeEntityInputView: makeEntityInputView)
self.panel.fromMenu = controller.fromMenu
self.panel.isStandalone = controller.isStandalone
@ -843,13 +847,14 @@ public class AttachmentController: ViewController {
public var getInputContainerNode: () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil }
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
self.context = context
self.updatedPresentationData = updatedPresentationData
self.chatLocation = chatLocation
self.buttons = buttons
self.initialButton = initialButton
self.fromMenu = fromMenu
self.makeEntityInputView = makeEntityInputView
super.init(navigationBarPresentationData: nil)
@ -877,7 +882,7 @@ public class AttachmentController: ViewController {
}
open override func loadDisplayNode() {
self.displayNode = Node(controller: self)
self.displayNode = Node(controller: self, makeEntityInputView: self.makeEntityInputView)
self.displayNodeDidLoad()
}

View File

@ -458,6 +458,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
private var presentationInterfaceState: ChatPresentationInterfaceState
private var interfaceInteraction: ChatPanelInterfaceInteraction?
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
private let containerNode: ASDisplayNode
private let backgroundNode: NavigationBackgroundNode
private let scrollNode: ASScrollNode
@ -496,9 +498,11 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
var mainButtonPressed: () -> Void = { }
init(context: AccountContext, chatLocation: ChatLocation, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?) {
init(context: AccountContext, chatLocation: ChatLocation, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
self.context = context
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.makeEntityInputView = makeEntityInputView
self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil)
@ -895,7 +899,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
if let strongSelf = self {
strongSelf.present(c)
}
})
}, makeEntityInputView: self.makeEntityInputView)
textInputPanelNode.interfaceInteraction = self.interfaceInteraction
textInputPanelNode.sendMessage = { [weak self] mode in
if let strongSelf = self {

View File

@ -543,7 +543,11 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
case .hide:
effectiveTopPanelHeight = 0.0
case .show, .hideOnScroll:
effectiveTopPanelHeight = topPanelHeight
if component.externalTopPanelContainer != nil {
effectiveTopPanelHeight = topPanelHeight
} else {
effectiveTopPanelHeight = 0.0
}
}
if let contentBackground = component.contentBackground {

View File

@ -41,6 +41,9 @@ swift_library(
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
"//submodules/TranslateUI:TranslateUI",
"//submodules/Utils/RangeSet:RangeSet",
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
],
visibility = [
"//visibility:public",

View File

@ -22,6 +22,9 @@ import UndoUI
import ManagedAnimationNode
import TelegramUniversalVideoContent
import InvisibleInkDustNode
import TextNodeWithEntities
import AnimationCache
import MultiAnimationRenderer
private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white)
private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white)
@ -133,10 +136,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
private let scrollWrapperNode: CaptionScrollWrapperNode
private let scrollNode: ASScrollNode
private let textNode: ImmediateTextNode
private var spoilerTextNode: ImmediateTextNode?
private let textNode: ImmediateTextNodeWithEntities
private var spoilerTextNode: ImmediateTextNodeWithEntities?
private var dustNode: InvisibleInkDustNode?
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private let authorNameNode: ASTextNode
private let dateNode: ASTextNode
private let backwardButton: PlaybackButtonNode
@ -319,7 +325,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.maskNode = ASDisplayNode()
self.textNode = ImmediateTextNode()
self.textNode = ImmediateTextNodeWithEntities()
self.textNode.maximumNumberOfLines = 0
self.textNode.linkHighlightColor = UIColor(rgb: 0x5ac8fa, alpha: 0.2)
@ -350,6 +356,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
self.statusNode.isUserInteractionEnabled = false
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
super.init()
self.addSubnode(self.contentNode)
@ -380,6 +391,15 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
}
}
self.textNode.arguments = TextNodeWithEntities.Arguments(
context: self.context,
cache: self.animationCache,
renderer: self.animationRenderer,
placeholderColor: defaultDarkPresentationTheme.list.mediaPlaceholderColor,
attemptSynchronous: false
)
self.textNode.visibility = true
self.contentNode.view.addSubview(self.deleteButton)
self.contentNode.view.addSubview(self.fullscreenButton)
self.contentNode.view.addSubview(self.actionButton)
@ -723,7 +743,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
private func updateSpoilers(textFrame: CGRect) {
if let textLayout = self.textNode.cachedLayout, !textLayout.spoilers.isEmpty {
if self.spoilerTextNode == nil {
let spoilerTextNode = ImmediateTextNode()
let spoilerTextNode = ImmediateTextNodeWithEntities()
spoilerTextNode.attributedText = textNode.attributedText
spoilerTextNode.maximumNumberOfLines = 0
spoilerTextNode.linkHighlightColor = UIColor(rgb: 0x5ac8fa, alpha: 0.2)

View File

@ -85,7 +85,7 @@ public final class EntityKeyboardComponent: Component {
public let hideInputUpdated: (Bool, Bool, Transition) -> Void
public let switchToTextInput: () -> Void
public let switchToGifSubject: (GifPagerContentComponent.Subject) -> Void
public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode
public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode?
public let deviceMetrics: DeviceMetrics
public let hiddenInputHeight: CGFloat
public let isExpanded: Bool
@ -103,7 +103,7 @@ public final class EntityKeyboardComponent: Component {
hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void,
switchToTextInput: @escaping () -> Void,
switchToGifSubject: @escaping (GifPagerContentComponent.Subject) -> Void,
makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode,
makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode?,
deviceMetrics: DeviceMetrics,
hiddenInputHeight: CGFloat,
isExpanded: Bool
@ -486,7 +486,8 @@ public final class EntityKeyboardComponent: Component {
)),
topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent(
theme: component.theme,
overflowHeight: component.hiddenInputHeight
overflowHeight: component.hiddenInputHeight,
displayBackground: component.externalTopPanelContainer == nil
)),
externalTopPanelContainer: component.externalTopPanelContainer,
bottomPanel: AnyComponent(EntityKeyboardBottomPanelComponent(

View File

@ -278,59 +278,64 @@ final class EntityKeyboardBottomPanelComponent: Component {
let navigateToContentId = panelEnvironment.navigateToContentId
for icon in panelEnvironment.contentIcons {
validIconIds.append(icon.id)
var iconTransition = transition
let iconView: ComponentHostView<Empty>
if let current = self.iconViews[icon.id] {
iconView = current
} else {
iconTransition = .immediate
iconView = ComponentHostView<Empty>()
self.iconViews[icon.id] = iconView
self.addSubview(iconView)
if panelEnvironment.contentIcons.count > 1 {
for icon in panelEnvironment.contentIcons {
validIconIds.append(icon.id)
var iconTransition = transition
let iconView: ComponentHostView<Empty>
if let current = self.iconViews[icon.id] {
iconView = current
} else {
iconTransition = .immediate
iconView = ComponentHostView<Empty>()
self.iconViews[icon.id] = iconView
self.addSubview(iconView)
}
let iconSize = iconView.update(
transition: iconTransition,
component: AnyComponent(BottomPanelIconComponent(
content: icon.component,
action: {
navigateToContentId(icon.id)
}
)),
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)
iconInfos[icon.id] = (size: iconSize, transition: iconTransition)
if !iconTotalSize.width.isZero {
iconTotalSize.width += iconSpacing
}
iconTotalSize.width += iconSize.width
iconTotalSize.height = max(iconTotalSize.height, iconSize.height)
}
let iconSize = iconView.update(
transition: iconTransition,
component: AnyComponent(BottomPanelIconComponent(
content: icon.component,
action: {
navigateToContentId(icon.id)
}
)),
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)
iconInfos[icon.id] = (size: iconSize, transition: iconTransition)
if !iconTotalSize.width.isZero {
iconTotalSize.width += iconSpacing
}
iconTotalSize.width += iconSize.width
iconTotalSize.height = max(iconTotalSize.height, iconSize.height)
}
var nextIconOrigin = CGPoint(x: floor((availableSize.width - iconTotalSize.width) / 2.0), y: floor((intrinsicHeight - iconTotalSize.height) / 2.0))
if component.bottomInset > 0.0 {
nextIconOrigin.y += 2.0
}
for icon in panelEnvironment.contentIcons {
guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else {
continue
if panelEnvironment.contentIcons.count > 1 {
for icon in panelEnvironment.contentIcons {
guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else {
continue
}
let iconFrame = CGRect(origin: nextIconOrigin, size: iconInfo.size)
iconInfo.transition.setFrame(view: iconView, frame: iconFrame, completion: nil)
if let activeContentId = activeContentId, activeContentId == icon.id {
self.highlightedIconBackgroundView.isHidden = false
transition.setFrame(view: self.highlightedIconBackgroundView, frame: iconFrame)
}
nextIconOrigin.x += iconInfo.size.width + iconSpacing
}
let iconFrame = CGRect(origin: nextIconOrigin, size: iconInfo.size)
iconInfo.transition.setFrame(view: iconView, frame: iconFrame, completion: nil)
if let activeContentId = activeContentId, activeContentId == icon.id {
self.highlightedIconBackgroundView.isHidden = false
transition.setFrame(view: self.highlightedIconBackgroundView, frame: iconFrame)
}
nextIconOrigin.x += iconInfo.size.width + iconSpacing
}
if activeContentId == nil {

View File

@ -32,13 +32,16 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
let theme: PresentationTheme
let overflowHeight: CGFloat
let displayBackground: Bool
init(
theme: PresentationTheme,
overflowHeight: CGFloat
overflowHeight: CGFloat,
displayBackground: Bool
) {
self.theme = theme
self.overflowHeight = overflowHeight
self.displayBackground = displayBackground
}
static func ==(lhs: EntityKeyboardTopContainerPanelComponent, rhs: EntityKeyboardTopContainerPanelComponent) -> Bool {
@ -48,6 +51,9 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
if lhs.overflowHeight != rhs.overflowHeight {
return false
}
if lhs.displayBackground != rhs.displayBackground {
return false
}
return true
}
@ -59,6 +65,9 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
}
final class View: UIView {
private var backgroundView: BlurredBackgroundView?
private var backgroundSeparatorView: UIView?
private var panelViews: [AnyHashable: PanelView] = [:]
private var component: EntityKeyboardTopContainerPanelComponent?
@ -183,6 +192,40 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
strongSelf.updateVisibilityFraction(value: fraction, transition: transition)
}
if component.displayBackground {
let backgroundView: BlurredBackgroundView
if let current = self.backgroundView {
backgroundView = current
} else {
backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.insertSubview(backgroundView, at: 0)
}
let backgroundSeparatorView: UIView
if let current = self.backgroundSeparatorView {
backgroundSeparatorView = current
} else {
backgroundSeparatorView = UIView()
self.insertSubview(backgroundSeparatorView, aboveSubview: backgroundView)
}
backgroundView.updateColor(color: component.theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(1.0), transition: .immediate)
backgroundView.update(size: CGSize(width: availableSize.width, height: height), transition: transition.containedViewLayoutTransition)
transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)))
backgroundSeparatorView.backgroundColor = component.theme.chat.inputPanel.panelSeparatorColor
transition.setFrame(view: backgroundSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: height), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
} else {
if let backgroundView = self.backgroundView {
self.backgroundView = nil
backgroundView.removeFromSuperview()
}
if let backgroundSeparatorView = self.backgroundSeparatorView {
self.backgroundSeparatorView = nil
backgroundSeparatorView.removeFromSuperview()
}
}
return CGSize(width: availableSize.width, height: height)
}

View File

@ -51,11 +51,11 @@ final class EntitySearchContentEnvironment: Equatable {
final class EntitySearchContentComponent: Component {
typealias EnvironmentType = EntitySearchContentEnvironment
let makeContainerNode: () -> EntitySearchContainerNode
let makeContainerNode: () -> EntitySearchContainerNode?
let dismissSearch: () -> Void
init(
makeContainerNode: @escaping () -> EntitySearchContainerNode,
makeContainerNode: @escaping () -> EntitySearchContainerNode?,
dismissSearch: @escaping () -> Void
) {
self.makeContainerNode = makeContainerNode
@ -78,30 +78,34 @@ final class EntitySearchContentComponent: Component {
}
func update(component: EntitySearchContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
let containerNode: EntitySearchContainerNode
let containerNode: EntitySearchContainerNode?
if let current = self.containerNode {
containerNode = current
} else {
containerNode = component.makeContainerNode()
self.containerNode = containerNode
self.addSubnode(containerNode)
if let containerNode = containerNode {
self.containerNode = containerNode
self.addSubnode(containerNode)
}
}
if let containerNode = containerNode {
let environmentValue = environment[EntitySearchContentEnvironment.self].value
transition.setFrame(view: containerNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
containerNode.updateLayout(
size: availableSize,
leftInset: 0.0,
rightInset: 0.0,
bottomInset: 0.0,
inputHeight: 0.0,
deviceMetrics: environmentValue.deviceMetrics,
transition: transition.containedViewLayoutTransition
)
containerNode.onCancel = {
component.dismissSearch()
transition.setFrame(view: containerNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
containerNode.updateLayout(
size: availableSize,
leftInset: 0.0,
rightInset: 0.0,
bottomInset: 0.0,
inputHeight: 0.0,
deviceMetrics: environmentValue.deviceMetrics,
transition: transition.containedViewLayoutTransition
)
containerNode.onCancel = {
component.dismissSearch()
}
}
return availableSize

View File

@ -118,7 +118,7 @@ public final class TextNodeWithEntities {
if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont {
string.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: range)
let itemSize = font.pointSize * 24.0 / 17.0 / CGFloat(range.length)
let itemSize = font.pointSize * 24.0 / 17.0 / CGFloat(min(2, range.length))
let runDelegateData = RunDelegateData(
ascent: font.ascender,

View File

@ -10732,7 +10732,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
let inputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { _ in })
let inputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { _ in }, makeEntityInputView: { [weak self] in
guard let strongSelf = self else {
return nil
}
return EntityInputView(context: strongSelf.context, isDark: true)
})
inputPanelNode.interfaceInteraction = interfaceInteraction
inputPanelNode.effectivePresentationInterfaceState = {
return presentationInterfaceState
@ -10967,7 +10973,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let currentFilesController = Atomic<AttachmentContainable?>(value: nil)
let currentLocationController = Atomic<AttachmentContainable?>(value: nil)
let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: strongSelf.chatLocation, buttons: buttons, initialButton: initialButton)
let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: strongSelf.chatLocation, buttons: buttons, initialButton: initialButton, makeEntityInputView: { [weak self] in
guard let strongSelf = self else {
return nil
}
return EntityInputView(context: strongSelf.context, isDark: false)
})
attachmentController.requestController = { [weak self, weak attachmentController] type, completion in
guard let strongSelf = self else {
return

View File

@ -540,14 +540,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
self.addSubnode(self.inputPanelContainerNode)
self.addSubnode(self.inputContextPanelContainer)
self.inputPanelContainerNode.addSubnode(self.inputPanelClippingNode)
self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundNode)
self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundSeparatorNode)
self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode)
self.contentContainerNode.addSubnode(self.inputContextPanelContainer)
self.addSubnode(self.messageTransitionNode)
self.contentContainerNode.addSubnode(self.navigateButtons)
self.contentContainerNode.addSubnode(self.presentationContextMarker)

View File

@ -21,6 +21,8 @@ import AudioToolbox
import UndoUI
import ContextUI
import GalleryUI
import AttachmentTextInputPanelNode
import TelegramPresentationData
private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = {
guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else {
@ -46,14 +48,14 @@ private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment,
final class ChatEntityKeyboardInputNode: ChatInputNode {
struct InputData: Equatable {
var emoji: EmojiPagerContentComponent
var stickers: EmojiPagerContentComponent
var gifs: GifPagerContentComponent
var stickers: EmojiPagerContentComponent?
var gifs: GifPagerContentComponent?
var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
init(
emoji: EmojiPagerContentComponent,
stickers: EmojiPagerContentComponent,
gifs: GifPagerContentComponent,
stickers: EmojiPagerContentComponent?,
gifs: GifPagerContentComponent?,
availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
) {
self.emoji = emoji
@ -63,7 +65,109 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction, chatPeerId: PeerId?) -> Signal<InputData, NoError> {
static func emojiInputData(context: AccountContext, inputInteraction: EmojiPagerContentComponent.InputInteraction, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError> {
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
hasPremium
)
|> map { view, hasPremium -> EmojiPagerContentComponent in
struct ItemGroup {
var supergroupId: AnyHashable
var id: AnyHashable
var isPremium: Bool
var items: [EmojiPagerContentComponent.Item]
}
var itemGroups: [ItemGroup] = []
var itemGroupIndexById: [AnyHashable: Int] = [:]
for (subgroupId, list) in staticEmojiMapping {
let groupId: AnyHashable = "static"
for emojiString in list {
let resultItem = EmojiPagerContentComponent.Item(
file: nil,
staticEmoji: emojiString,
subgroupId: subgroupId.rawValue
)
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, isPremium: false, items: [resultItem]))
}
}
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
let resultItem = EmojiPagerContentComponent.Item(
file: item.file,
staticEmoji: nil,
subgroupId: nil
)
let supergroupId = entry.index.collectionId
let groupId: AnyHashable = supergroupId
let isPremium: Bool = item.file.isPremiumEmoji && !hasPremium
if isPremium && isPremiumDisabled {
continue
}
/*if isPremium {
groupId = "\(supergroupId)-p"
} else {
groupId = supergroupId
}*/
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, isPremium: isPremium, items: [resultItem]))
}
}
return EmojiPagerContentComponent(
id: "emoji",
context: context,
animationCache: animationCache,
animationRenderer: animationRenderer,
inputInteraction: inputInteraction,
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
var title: String?
if group.id == AnyHashable("recent") {
//TODO:localize
title = "Recently Used"
} else {
for (id, info, _) in view.collectionInfos {
if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo {
title = info.title
break
}
}
}
return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: title, isPremium: group.isPremium, displayPremiumBadges: false, items: group.items)
},
itemLayoutType: .compact
)
}
return emojiItems
}
static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?) -> Signal<InputData, NoError> {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
@ -177,9 +281,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
chatPeerId: chatPeerId
)
let stickerInputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak interfaceInteraction] item, view, rect, layer in
performItemAction: { [weak controllerInteraction, weak interfaceInteraction] item, view, rect, layer in
let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
guard let interfaceInteraction = interfaceInteraction else {
guard let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
return
}
if let file = item.file {
@ -249,98 +353,13 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
animationRenderer = MultiAnimationRendererImpl()
//}
let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
let emojiItems = emojiInputData(context: context, inputInteraction: emojiInputInteraction, animationCache: animationCache, animationRenderer: animationRenderer)
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
hasPremium
)
|> map { view, hasPremium -> EmojiPagerContentComponent in
struct ItemGroup {
var supergroupId: AnyHashable
var id: AnyHashable
var isPremium: Bool
var items: [EmojiPagerContentComponent.Item]
}
var itemGroups: [ItemGroup] = []
var itemGroupIndexById: [AnyHashable: Int] = [:]
for (subgroupId, list) in staticEmojiMapping {
let groupId: AnyHashable = "static"
for emojiString in list {
let resultItem = EmojiPagerContentComponent.Item(
file: nil,
staticEmoji: emojiString,
subgroupId: subgroupId.rawValue
)
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, isPremium: false, items: [resultItem]))
}
}
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
let resultItem = EmojiPagerContentComponent.Item(
file: item.file,
staticEmoji: nil,
subgroupId: nil
)
let supergroupId = entry.index.collectionId
let groupId: AnyHashable = supergroupId
let isPremium: Bool = item.file.isPremiumEmoji && !hasPremium
if isPremium && isPremiumDisabled {
continue
}
/*if isPremium {
groupId = "\(supergroupId)-p"
} else {
groupId = supergroupId
}*/
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, isPremium: isPremium, items: [resultItem]))
}
}
return EmojiPagerContentComponent(
id: "emoji",
context: context,
animationCache: animationCache,
animationRenderer: animationRenderer,
inputInteraction: emojiInputInteraction,
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
var title: String?
if group.id == AnyHashable("recent") {
//TODO:localize
title = "Recently Used"
} else {
for (id, info, _) in view.collectionInfos {
if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo {
title = info.title
break
}
}
}
return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: title, isPremium: group.isPremium, displayPremiumBadges: false, items: group.items)
},
itemLayoutType: .compact
)
}
let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
let stickerItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000),
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000),
hasPremium
)
|> map { view, hasPremium -> EmojiPagerContentComponent in
@ -604,7 +623,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
private var currentInputData: InputData
private var inputDataDisposable: Disposable?
private let controllerInteraction: ChatControllerInteraction
private let controllerInteraction: ChatControllerInteraction?
private var inputNodeInteraction: ChatMediaInputNodeInteraction?
@ -617,6 +636,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
return self.externalTopPanelContainerImpl
}
var switchToTextInput: (() -> Void)?
private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)?
private var gifMode: GifPagerContentComponent.Subject = .recent {
@ -811,7 +832,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
private let gifComponent = Promise<GifPagerContentComponent>()
private var gifInputInteraction: GifPagerContentComponent.InputInteraction?
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction) {
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction?) {
self.context = context
self.currentInputData = currentInputData
self.defaultToEmojiTab = defaultToEmojiTab
@ -898,6 +919,15 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
)
self.switchToTextInput = { [weak self] in
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
return
}
controllerInteraction.updateInputMode { _ in
return .text
}
}
self.reloadGifContext()
}
@ -996,12 +1026,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
},
switchToTextInput: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.controllerInteraction.updateInputMode { _ in
return .text
}
self?.switchToTextInput?()
},
switchToGifSubject: { [weak self] subject in
guard let strongSelf = self else {
@ -1009,7 +1034,11 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
strongSelf.gifMode = subject
},
makeSearchContainerNode: { content in
makeSearchContainerNode: { [weak controllerInteraction] content in
guard let controllerInteraction = controllerInteraction else {
return nil
}
let mappedMode: ChatMediaInputSearchMode
switch content {
case .stickers:
@ -1074,7 +1103,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}, action: { _, f in
f(.default)
if isSaved {
let _ = self?.controllerInteraction.sendGif(FileMediaReference.savedGif(media: file), sourceView, sourceRect, false, false)
let _ = self?.controllerInteraction?.sendGif(FileMediaReference.savedGif(media: file), sourceView, sourceRect, false, false)
}/* else if let (collection, result) = file.contextResult {
let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false)
}*/
@ -1141,7 +1170,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|> deliverOnMainQueue).start(next: { result in
switch result {
case .generic:
controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String
@ -1150,10 +1179,10 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
} else {
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string
}
controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in
controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in
if case .info = action {
let controller = PremiumIntroScreen(context: context, source: .savedGifs)
controllerInteraction.navigationController()?.pushViewController(controller)
controllerInteraction?.navigationController()?.pushViewController(controller)
return true
}
return false
@ -1164,7 +1193,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
strongSelf.controllerInteraction.presentGlobalOverlayController(contextController, nil)
strongSelf.controllerInteraction?.presentGlobalOverlayController(contextController, nil)
})
}
}
@ -1202,3 +1231,220 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
}
}
}
final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback {
private let context: AccountContext
public var insertText: ((NSAttributedString) -> Void)?
public var deleteBackwards: (() -> Void)?
public var switchToKeyboard: (() -> Void)?
public var presentController: ((ViewController) -> Void)?
private var presentationData: PresentationData
private var inputNode: ChatEntityKeyboardInputNode?
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
init(
context: AccountContext,
isDark: Bool
) {
self.context = context
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
if isDark {
self.presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme)
}
//super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), inputViewStyle: .default)
super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)))
self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.clipsToBounds = true
let inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] item, _, _, _ in
guard let strongSelf = self else {
return
}
let hasPremium = strongSelf.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
guard let strongSelf = self else {
return
}
if let file = item.file {
var text = "."
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, displayText, packReference):
text = displayText
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(stickerPack: packReference, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
if file.isPremiumEmoji && !hasPremium {
//TODO:localize
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
strongSelf.presentController?(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: "Subscribe to Telegram Premium to unlock this emoji.", undoText: "More", customAction: {
guard let strongSelf = self else {
return
}
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: strongSelf.context, subject: .premiumStickers, action: {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .stickers)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
strongSelf.presentController?(controller)
}), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }))
return
}
if let emojiAttribute = emojiAttribute {
AudioServicesPlaySystemSound(0x450)
strongSelf.insertText?(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]))
}
} else if let staticEmoji = item.staticEmoji {
AudioServicesPlaySystemSound(0x450)
strongSelf.insertText?(NSAttributedString(string: staticEmoji, attributes: [:]))
}
})
},
deleteBackwards: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.deleteBackwards?()
},
openStickerSettings: {
},
openPremiumSection: {
},
pushController: { _ in
},
presentController: { _ in
},
presentGlobalOverlayController: { _ in
},
navigationController: {
return nil
},
sendSticker: nil,
chatPeerId: nil
)
let semaphore = DispatchSemaphore(value: 0)
var emojiComponent: EmojiPagerContentComponent?
let _ = ChatEntityKeyboardInputNode.emojiInputData(context: context, inputInteraction: inputInteraction, animationCache: self.animationCache, animationRenderer: self.animationRenderer).start(next: { value in
emojiComponent = value
semaphore.signal()
})
semaphore.wait()
if let emojiComponent = emojiComponent {
let inputNode = ChatEntityKeyboardInputNode(
context: self.context,
currentInputData: ChatEntityKeyboardInputNode.InputData(
emoji: emojiComponent,
stickers: nil,
gifs: nil,
availableGifSearchEmojies: []
),
updatedInputData: .never(),
defaultToEmojiTab: true,
controllerInteraction: nil
)
self.inputNode = inputNode
inputNode.externalTopPanelContainerImpl = nil
inputNode.switchToTextInput = { [weak self] in
self?.switchToKeyboard?()
}
self.addSubnode(inputNode)
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
guard let inputNode = self.inputNode else {
return
}
for view in self.subviews {
if view !== inputNode.view {
view.isHidden = true
}
}
let bottomInset: CGFloat
if #available(iOS 11.0, *) {
bottomInset = max(0.0, UIScreen.main.bounds.height - (self.window?.safeAreaLayoutGuide.layoutFrame.maxY ?? 10000.0))
} else {
bottomInset = 0.0
}
let presentationInterfaceState = ChatPresentationInterfaceState(
chatWallpaper: .builtin(WallpaperSettings()),
theme: self.presentationData.theme,
strings: self.presentationData.strings,
dateTimeFormat: self.presentationData.dateTimeFormat,
nameDisplayOrder: self.presentationData.nameDisplayOrder,
limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 },
fontSize: self.presentationData.chatFontSize,
bubbleCorners: self.presentationData.chatBubbleCorners,
accountPeerId: self.context.account.peerId,
mode: .standard(previewing: false),
chatLocation: .peer(id: self.context.account.peerId),
subject: nil,
peerNearbyData: nil,
greetingData: nil,
pendingUnpinnedAllMessages: false,
activeGroupCallInfo: nil,
hasActiveGroupCall: false,
importState: nil
)
let _ = inputNode.updateLayout(
width: self.bounds.width,
leftInset: 0.0,
rightInset: 0.0,
bottomInset: bottomInset,
standardInputHeight: self.bounds.height,
inputHeight: self.bounds.height,
maximumHeight: self.bounds.height,
inputPanelHeight: 0.0,
transition: .immediate,
interfaceState: presentationInterfaceState,
deviceMetrics: DeviceMetrics.iPhone12,
isVisible: true,
isExpanded: false
)
inputNode.frame = self.bounds
}
}

View File

@ -390,7 +390,9 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.addSubnode(forwardAccessoryPanelNode)
self.forwardAccessoryPanelNode = forwardAccessoryPanelNode
let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, presentController: { [weak self] c in self?.present(c, nil) })
let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, presentController: { [weak self] c in self?.present(c, nil) }, makeEntityInputView: {
return nil
})
textInputPanelNode.interfaceInteraction = self.interfaceInteraction
textInputPanelNode.sendMessage = { [weak self] mode in
guard let strongSelf = self else {

View File

@ -70,6 +70,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
self.textNode.maximumNumberOfLines = 1
self.textNode.displaysAsynchronously = false
self.textNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
self.textNode.visibility = true
if let animationCache = animationCache, let animationRenderer = animationRenderer {
self.textNode.arguments = TextNodeWithEntities.Arguments(

View File

@ -1212,7 +1212,9 @@ private final class WebAppContextReferenceContentSource: ContextReferenceContent
}
public func standaloneWebAppController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, params: WebAppParameters, openUrl: @escaping (String) -> Void, getInputContainerNode: @escaping () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil }, completion: @escaping () -> Void = {}, willDismiss: @escaping () -> Void = {}, didDismiss: @escaping () -> Void = {}, getNavigationController: @escaping () -> NavigationController? = { return nil }) -> ViewController {
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.fromMenu)
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.fromMenu, makeEntityInputView: {
return nil
})
controller.getInputContainerNode = getInputContainerNode
controller.requestController = { _, present in
let webAppController = WebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil)