Animated emoji

This commit is contained in:
Ali 2022-05-21 01:16:29 +03:00
parent 352916f866
commit 274f05d80a
31 changed files with 953 additions and 147 deletions

View File

@ -822,6 +822,10 @@ public protocol AccountContext: AnyObject {
var cachedGroupCallContexts: AccountGroupCallContextCache { get } var cachedGroupCallContexts: AccountGroupCallContextCache { get }
var meshAnimationCache: MeshAnimationCache { get } var meshAnimationCache: MeshAnimationCache { get }
var animatedEmojiStickers: [String: [StickerPackItem]] { get }
var userLimits: EngineConfiguration.UserLimits { get }
func storeSecureIdPassword(password: String) func storeSecureIdPassword(password: String)
func getStoredSecureIdPassword() -> String? func getStoredSecureIdPassword() -> String?

View File

@ -144,7 +144,7 @@ public protocol AnimatedStickerNodeSource {
var isVideo: Bool { get } var isVideo: Bool { get }
func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError> func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError>
func directDataPath() -> Signal<String, NoError> func directDataPath(attemptSynchronously: Bool) -> Signal<String?, NoError>
} }
public final class AnimatedStickerNode: ASDisplayNode { public final class AnimatedStickerNode: ASDisplayNode {
@ -304,9 +304,10 @@ public final class AnimatedStickerNode: ASDisplayNode {
strongSelf.play(firstFrame: true) strongSelf.play(firstFrame: true)
} }
} }
self.disposable.set((source.directDataPath() self.disposable.set((source.directDataPath(attemptSynchronously: false)
|> filter { $0 != nil }
|> deliverOnMainQueue).start(next: { path in |> deliverOnMainQueue).start(next: { path in
f(path) f(path!)
})) }))
case .cached: case .cached:
self.disposable.set((source.cachedDataPath(width: width, height: height) self.disposable.set((source.cachedDataPath(width: width, height: height)

View File

@ -13,6 +13,27 @@
#import <tgmath.h> #import <tgmath.h>
@interface ASCustomLayoutManager : NSLayoutManager
@end
@implementation ASCustomLayoutManager
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {
/*CGGlyph glyph = [self glyphAtIndex:glyphsToShow.location];
if (glyph) {
}
CGRect bounds = [self boundingRectForGlyphRange:glyphsToShow inTextContainer:[self textContainerForGlyphAtIndex:glyphsToShow.location effectiveRange:nil]];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [UIColor grayColor].CGColor);
CGContextFillRect(context, bounds);*/
[super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin];
}
@end
@interface ASTextKitComponentsTextView () { @interface ASTextKitComponentsTextView () {
// Prevent UITextView from updating contentOffset while deallocating: https://github.com/TextureGroup/Texture/issues/860 // Prevent UITextView from updating contentOffset while deallocating: https://github.com/TextureGroup/Texture/issues/860
BOOL _deallocating; BOOL _deallocating;
@ -83,7 +104,7 @@
return [self componentsWithTextStorage:textStorage return [self componentsWithTextStorage:textStorage
textContainerSize:textContainerSize textContainerSize:textContainerSize
layoutManager:[[NSLayoutManager alloc] init]]; layoutManager:[[ASCustomLayoutManager alloc] init]];
} }
+ (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage + (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage

View File

@ -28,6 +28,7 @@ swift_library(
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState", "//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
"//submodules/Pasteboard:Pasteboard", "//submodules/Pasteboard:Pasteboard",
"//submodules/ContextUI:ContextUI", "//submodules/ContextUI:ContextUI",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -18,6 +18,7 @@ import InvisibleInkDustNode
import TextInputMenu import TextInputMenu
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import Pasteboard import Pasteboard
import EmojiTextAttachmentView
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
@ -208,7 +209,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
} }
textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count)
self.updatingInputState = false self.updatingInputState = false
self.updateTextNodeText(animated: animated) self.updateTextNodeText(animated: animated)
@ -241,6 +242,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
private var spoilersRevealed = false private var spoilersRevealed = false
private var emojiViewProvider: ((String) -> UIView)?
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) {
self.context = context self.context = context
self.presentationInterfaceState = presentationInterfaceState self.presentationInterfaceState = presentationInterfaceState
@ -311,6 +314,14 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
} }
self.textInputBackgroundNode.view.addGestureRecognizer(recognizer) self.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
self.emojiViewProvider = { [weak self] emoji in
guard let strongSelf = self, let file = strongSelf.context.animatedEmojiStickers[emoji]?.first?.file else {
return UIView()
}
return EmojiTextAttachmentView(context: context, file: file)
}
self.updateSendButtonEnabled(isCaption || isAttachment, animated: false) self.updateSendButtonEnabled(isCaption || isAttachment, animated: false)
} }
@ -741,7 +752,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
@objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { @objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState {
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed) refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
self.updateSpoiler() self.updateSpoiler()
@ -876,9 +887,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
textInputNode.textView.isScrollEnabled = false textInputNode.textView.isScrollEnabled = false
refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed) refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
if textInputNode.textView.subviews.count > 1, animated { if textInputNode.textView.subviews.count > 1, animated {
let containerView = textInputNode.textView.subviews[1] let containerView = textInputNode.textView.subviews[1]
@ -968,7 +979,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
let textFont = Font.regular(baseFontSize) let textFont = Font.regular(baseFontSize)
let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
let attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false) let attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
let range = (attributedText.string as NSString).range(of: "\n") let range = (attributedText.string as NSString).range(of: "\n")
if range.location != NSNotFound { if range.location != NSNotFound {
@ -1234,7 +1245,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
} }
} }
} }
if cleanText != text { if cleanText != text {
let string = NSMutableAttributedString(attributedString: editableTextNode.attributedText ?? NSAttributedString()) let string = NSMutableAttributedString(attributedString: editableTextNode.attributedText ?? NSAttributedString())
var textColor: UIColor = .black var textColor: UIColor = .black
@ -1245,7 +1256,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
} }
let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
string.replaceCharacters(in: range, with: cleanReplacementString) string.replaceCharacters(in: range, with: cleanReplacementString)
self.textInputNode?.attributedText = string self.textInputNode?.attributedText = string
self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0) self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0)

View File

@ -228,7 +228,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe
rightInset += inlineAction.icon.size.width + 8.0 rightInset += inlineAction.icon.size.width + 8.0
} }
let itemText = textAttributedStringForStateText(item.text, fontSize: 17.0, textColor: item.presentationData.theme.chat.inputPanel.primaryTextColor, accentTextColor: item.presentationData.theme.chat.inputPanel.panelControlAccentColor, writingDirection: nil, spoilersRevealed: false) let itemText = textAttributedStringForStateText(item.text, fontSize: 17.0, textColor: item.presentationData.theme.chat.inputPanel.primaryTextColor, accentTextColor: item.presentationData.theme.chat.inputPanel.panelControlAccentColor, writingDirection: nil, spoilersRevealed: false, availableEmojis: Set(), emojiViewProvider: nil)
let measureText = NSMutableAttributedString(attributedString: itemText) let measureText = NSMutableAttributedString(attributedString: itemText)
let measureRawString = measureText.string let measureRawString = measureText.string
if measureRawString.hasSuffix("\n") || measureRawString.isEmpty { if measureRawString.hasSuffix("\n") || measureRawString.isEmpty {
@ -294,11 +294,11 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe
if let currentText = strongSelf.textNode.attributedText { if let currentText = strongSelf.textNode.attributedText {
if currentText.string != attributedText.string || updatedTheme != nil { if currentText.string != attributedText.string || updatedTheme != nil {
strongSelf.textNode.attributedText = attributedText strongSelf.textNode.attributedText = attributedText
refreshGenericTextInputAttributes(strongSelf.textNode, theme: item.presentationData.theme, baseFontSize: 17.0) refreshGenericTextInputAttributes(strongSelf.textNode, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil)
} }
} else { } else {
strongSelf.textNode.attributedText = attributedText strongSelf.textNode.attributedText = attributedText
refreshGenericTextInputAttributes(strongSelf.textNode, theme: item.presentationData.theme, baseFontSize: 17.0) refreshGenericTextInputAttributes(strongSelf.textNode, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil)
} }
if strongSelf.backgroundNode.supernode == nil { if strongSelf.backgroundNode.supernode == nil {
@ -514,7 +514,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe
public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
if let item = self.item { if let item = self.item {
if let _ = self.textNode.attributedText { if let _ = self.textNode.attributedText {
refreshGenericTextInputAttributes(editableTextNode, theme: item.presentationData.theme, baseFontSize: 17.0) refreshGenericTextInputAttributes(editableTextNode, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil)
let updatedText = stateAttributedStringForText(self.textNode.attributedText!) let updatedText = stateAttributedStringForText(self.textNode.attributedText!)
item.textUpdated(updatedText) item.textUpdated(updatedText)
} else { } else {
@ -544,7 +544,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe
} }
refreshChatTextInputTypingAttributes(editableTextNode, theme: item.presentationData.theme, baseFontSize: 17.0) refreshChatTextInputTypingAttributes(editableTextNode, theme: item.presentationData.theme, baseFontSize: 17.0)
refreshGenericTextInputAttributes(editableTextNode, theme: item.presentationData.theme, baseFontSize: 17.0) refreshGenericTextInputAttributes(editableTextNode, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil)
} }
} }
@ -591,7 +591,7 @@ private func chatTextInputAddFormattingAttribute(item: CreatePollTextInputItem,
textNode.selectedRange = nsRange textNode.selectedRange = nsRange
refreshChatTextInputTypingAttributes(textNode, theme: theme, baseFontSize: 17.0) refreshChatTextInputTypingAttributes(textNode, theme: theme, baseFontSize: 17.0)
refreshGenericTextInputAttributes(textNode, theme: theme, baseFontSize: 17.0) refreshGenericTextInputAttributes(textNode, theme: theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil)
let updatedText = stateAttributedStringForText(textNode.attributedText!) let updatedText = stateAttributedStringForText(textNode.attributedText!)
item.textUpdated(updatedText) item.textUpdated(updatedText)

View File

@ -895,6 +895,8 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
let faq = SettingsSearchableItem(id: .faq(0), title: strings.Settings_FAQ, alternate: synonyms(strings.SettingsSearch_Synonyms_FAQ), icon: .faq, breadcrumbs: [], present: { context, navigationController, present in let faq = SettingsSearchableItem(id: .faq(0), title: strings.Settings_FAQ, alternate: synonyms(strings.SettingsSearch_Synonyms_FAQ), icon: .faq, breadcrumbs: [], present: { context, navigationController, present in
#if DEBUG
#else
let _ = (cachedFaqInstantPage(context: context) let _ = (cachedFaqInstantPage(context: context)
|> deliverOnMainQueue).start(next: { resolvedUrl in |> deliverOnMainQueue).start(next: { resolvedUrl in
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in
@ -902,6 +904,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
present(.push, controller) present(.push, controller)
}, dismissInput: {}, contentContext: nil) }, dismissInput: {}, contentContext: nil)
}) })
#endif
}) })
allItems.append(faq) allItems.append(faq)

View File

@ -18,11 +18,11 @@ public final class AnimatedStickerNodeLocalFileSource: AnimatedStickerNodeSource
self.name = name self.name = name
} }
public func directDataPath() -> Signal<String, NoError> { public func directDataPath(attemptSynchronously: Bool) -> Signal<String?, NoError> {
if let path = self.path { if let path = self.path {
return .single(path) return .single(path)
} else { } else {
return .never() return .single(nil)
} }
} }
@ -30,6 +30,10 @@ public final class AnimatedStickerNodeLocalFileSource: AnimatedStickerNodeSource
return .never() return .never()
} }
func maybeCachedDataPath(width: Int, height: Int) -> (String, Bool)? {
return nil
}
public var path: String? { public var path: String? {
if let path = getAppBundle().path(forResource: self.name, ofType: "tgs") { if let path = getAppBundle().path(forResource: self.name, ofType: "tgs") {
return path return path
@ -64,13 +68,14 @@ public final class AnimatedStickerResourceSource: AnimatedStickerNodeSource {
} }
} }
public func directDataPath() -> Signal<String, NoError> { public func directDataPath(attemptSynchronously: Bool) -> Signal<String?, NoError> {
return self.account.postbox.mediaBox.resourceData(self.resource) return self.account.postbox.mediaBox.resourceData(self.resource, attemptSynchronously: attemptSynchronously)
|> filter { data in |> map { data -> String? in
return data.complete if data.complete {
} return data.path
|> map { data -> String in } else {
return data.path return nil
}
} }
} }
} }

View File

@ -11,6 +11,7 @@ public struct UserLimitsConfiguration: Equatable {
public let maxFolderChatsCount: Int32 public let maxFolderChatsCount: Int32
public let maxCaptionLengthCount: Int32 public let maxCaptionLengthCount: Int32
public let maxUploadFileParts: Int32 public let maxUploadFileParts: Int32
public let maxAnimatedEmojisInText: Int32
public static var defaultValue: UserLimitsConfiguration { public static var defaultValue: UserLimitsConfiguration {
return UserLimitsConfiguration( return UserLimitsConfiguration(
@ -22,7 +23,8 @@ public struct UserLimitsConfiguration: Equatable {
maxFoldersCount: 10, maxFoldersCount: 10,
maxFolderChatsCount: 100, maxFolderChatsCount: 100,
maxCaptionLengthCount: 1024, maxCaptionLengthCount: 1024,
maxUploadFileParts: 4000 maxUploadFileParts: 4000,
maxAnimatedEmojisInText: 10
) )
} }
@ -35,7 +37,8 @@ public struct UserLimitsConfiguration: Equatable {
maxFoldersCount: Int32, maxFoldersCount: Int32,
maxFolderChatsCount: Int32, maxFolderChatsCount: Int32,
maxCaptionLengthCount: Int32, maxCaptionLengthCount: Int32,
maxUploadFileParts: Int32 maxUploadFileParts: Int32,
maxAnimatedEmojisInText: Int32
) { ) {
self.maxPinnedChatCount = maxPinnedChatCount self.maxPinnedChatCount = maxPinnedChatCount
self.maxChannelsCount = maxChannelsCount self.maxChannelsCount = maxChannelsCount
@ -46,6 +49,7 @@ public struct UserLimitsConfiguration: Equatable {
self.maxFolderChatsCount = maxFolderChatsCount self.maxFolderChatsCount = maxFolderChatsCount
self.maxCaptionLengthCount = maxCaptionLengthCount self.maxCaptionLengthCount = maxCaptionLengthCount
self.maxUploadFileParts = maxUploadFileParts self.maxUploadFileParts = maxUploadFileParts
self.maxAnimatedEmojisInText = maxAnimatedEmojisInText
} }
} }
@ -62,6 +66,14 @@ extension UserLimitsConfiguration {
} }
} }
func getGeneralValue(_ key: String, orElse defaultValue: Int32) -> Int32 {
if let value = appConfiguration.data?[key] as? Double {
return Int32(value)
} else {
return defaultValue
}
}
self.maxPinnedChatCount = getValue("dialogs_pinned_limit", orElse: defaultValue.maxPinnedChatCount) self.maxPinnedChatCount = getValue("dialogs_pinned_limit", orElse: defaultValue.maxPinnedChatCount)
self.maxChannelsCount = getValue("channels_limit", orElse: defaultValue.maxPinnedChatCount) self.maxChannelsCount = getValue("channels_limit", orElse: defaultValue.maxPinnedChatCount)
self.maxPublicLinksCount = getValue("channels_public_limit", orElse: defaultValue.maxPinnedChatCount) self.maxPublicLinksCount = getValue("channels_public_limit", orElse: defaultValue.maxPinnedChatCount)
@ -71,5 +83,6 @@ extension UserLimitsConfiguration {
self.maxFolderChatsCount = getValue("dialog_filters_chats_limit", orElse: defaultValue.maxPinnedChatCount) self.maxFolderChatsCount = getValue("dialog_filters_chats_limit", orElse: defaultValue.maxPinnedChatCount)
self.maxCaptionLengthCount = getValue("caption_length_limit", orElse: defaultValue.maxCaptionLengthCount) self.maxCaptionLengthCount = getValue("caption_length_limit", orElse: defaultValue.maxCaptionLengthCount)
self.maxUploadFileParts = getValue("upload_max_fileparts", orElse: defaultValue.maxUploadFileParts) self.maxUploadFileParts = getValue("upload_max_fileparts", orElse: defaultValue.maxUploadFileParts)
self.maxAnimatedEmojisInText = getGeneralValue("message_animated_emoji_max", orElse: defaultValue.maxAnimatedEmojisInText)
} }
} }

View File

@ -1,30 +1,30 @@
import Postbox import Postbox
public class AudioTranscriptionMessageAttribute: MessageAttribute, Equatable { public class AudioTranscriptionMessageAttribute: MessageAttribute, Equatable {
public let locale: String public let id: Int64
public let text: String public let text: String
public var associatedPeerIds: [PeerId] { public var associatedPeerIds: [PeerId] {
return [] return []
} }
public init(locale: String, text: String) { public init(id: Int64, text: String) {
self.locale = locale self.id = id
self.text = text self.text = text
} }
required public init(decoder: PostboxDecoder) { required public init(decoder: PostboxDecoder) {
self.locale = decoder.decodeStringForKey("locale", orElse: "") self.id = decoder.decodeInt64ForKey("id", orElse: 0)
self.text = decoder.decodeStringForKey("text", orElse: "") self.text = decoder.decodeStringForKey("text", orElse: "")
} }
public func encode(_ encoder: PostboxEncoder) { public func encode(_ encoder: PostboxEncoder) {
encoder.encodeString(self.locale, forKey: "locale") encoder.encodeInt64(self.id, forKey: "id")
encoder.encodeString(self.text, forKey: "text") encoder.encodeString(self.text, forKey: "text")
} }
public static func ==(lhs: AudioTranscriptionMessageAttribute, rhs: AudioTranscriptionMessageAttribute) -> Bool { public static func ==(lhs: AudioTranscriptionMessageAttribute, rhs: AudioTranscriptionMessageAttribute) -> Bool {
if lhs.locale != rhs.locale { if lhs.id != rhs.id {
return false return false
} }
if lhs.text != rhs.text { if lhs.text != rhs.text {

View File

@ -57,6 +57,7 @@ public enum EngineConfiguration {
public let maxFolderChatsCount: Int32 public let maxFolderChatsCount: Int32
public let maxCaptionLengthCount: Int32 public let maxCaptionLengthCount: Int32
public let maxUploadFileParts: Int32 public let maxUploadFileParts: Int32
public let maxAnimatedEmojisInText: Int32
public static var defaultValue: UserLimits { public static var defaultValue: UserLimits {
return UserLimits(UserLimitsConfiguration.defaultValue) return UserLimits(UserLimitsConfiguration.defaultValue)
@ -71,7 +72,8 @@ public enum EngineConfiguration {
maxFoldersCount: Int32, maxFoldersCount: Int32,
maxFolderChatsCount: Int32, maxFolderChatsCount: Int32,
maxCaptionLengthCount: Int32, maxCaptionLengthCount: Int32,
maxUploadFileParts: Int32 maxUploadFileParts: Int32,
maxAnimatedEmojisInText: Int32
) { ) {
self.maxPinnedChatCount = maxPinnedChatCount self.maxPinnedChatCount = maxPinnedChatCount
self.maxChannelsCount = maxChannelsCount self.maxChannelsCount = maxChannelsCount
@ -82,6 +84,7 @@ public enum EngineConfiguration {
self.maxFolderChatsCount = maxFolderChatsCount self.maxFolderChatsCount = maxFolderChatsCount
self.maxCaptionLengthCount = maxCaptionLengthCount self.maxCaptionLengthCount = maxCaptionLengthCount
self.maxUploadFileParts = maxUploadFileParts self.maxUploadFileParts = maxUploadFileParts
self.maxAnimatedEmojisInText = maxAnimatedEmojisInText
} }
} }
} }
@ -105,7 +108,7 @@ extension EngineConfiguration.Limits {
} }
} }
extension EngineConfiguration.UserLimits { public extension EngineConfiguration.UserLimits {
init(_ userLimitsConfiguration: UserLimitsConfiguration) { init(_ userLimitsConfiguration: UserLimitsConfiguration) {
self.init( self.init(
maxPinnedChatCount: userLimitsConfiguration.maxPinnedChatCount, maxPinnedChatCount: userLimitsConfiguration.maxPinnedChatCount,
@ -116,7 +119,8 @@ extension EngineConfiguration.UserLimits {
maxFoldersCount: userLimitsConfiguration.maxFoldersCount, maxFoldersCount: userLimitsConfiguration.maxFoldersCount,
maxFolderChatsCount: userLimitsConfiguration.maxFolderChatsCount, maxFolderChatsCount: userLimitsConfiguration.maxFolderChatsCount,
maxCaptionLengthCount: userLimitsConfiguration.maxCaptionLengthCount, maxCaptionLengthCount: userLimitsConfiguration.maxCaptionLengthCount,
maxUploadFileParts: userLimitsConfiguration.maxUploadFileParts maxUploadFileParts: userLimitsConfiguration.maxUploadFileParts,
maxAnimatedEmojisInText: userLimitsConfiguration.maxAnimatedEmojisInText
) )
} }
} }

View File

@ -322,10 +322,14 @@ public extension TelegramEngine {
return _internal_translate(network: self.account.network, text: text, fromLang: fromLang, toLang: toLang) return _internal_translate(network: self.account.network, text: text, fromLang: fromLang, toLang: toLang)
} }
public func transcribeAudio(messageId: MessageId) -> Signal<EngineAudioTranscriptionResult?, NoError> { public func transcribeAudio(messageId: MessageId) -> Signal<EngineAudioTranscriptionResult, NoError> {
return _internal_transcribeAudio(postbox: self.account.postbox, network: self.account.network, messageId: messageId) return _internal_transcribeAudio(postbox: self.account.postbox, network: self.account.network, messageId: messageId)
} }
public func rateAudioTranscription(messageId: MessageId, id: Int64, isGood: Bool) -> Signal<Never, NoError> {
return _internal_rateAudioTranscription(postbox: self.account.postbox, network: self.account.network, messageId: messageId, id: id, isGood: isGood)
}
public func requestWebView(peerId: PeerId, botId: PeerId, url: String?, payload: String?, themeParams: [String: Any]?, fromMenu: Bool, replyToMessageId: MessageId?) -> Signal<RequestWebViewResult, RequestWebViewError> { public func requestWebView(peerId: PeerId, botId: PeerId, url: String?, payload: String?, themeParams: [String: Any]?, fromMenu: Bool, replyToMessageId: MessageId?) -> Signal<RequestWebViewResult, RequestWebViewError> {
return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, botId: botId, url: url, payload: payload, themeParams: themeParams, fromMenu: fromMenu, replyToMessageId: replyToMessageId) return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, botId: botId, url: url, payload: payload, themeParams: themeParams, fromMenu: fromMenu, replyToMessageId: replyToMessageId)
} }

View File

@ -29,32 +29,70 @@ func _internal_translate(network: Network, text: String, fromLang: String?, toLa
} }
} }
public struct EngineAudioTranscriptionResult { public enum EngineAudioTranscriptionResult {
public var id: Int64 public struct Success {
public var text: String public var id: Int64
public var text: String
public init(id: Int64, text: String) {
self.id = id
self.text = text
}
}
case success(Success)
case error
} }
func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: MessageId) -> Signal<EngineAudioTranscriptionResult?, NoError> { func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: MessageId) -> Signal<EngineAudioTranscriptionResult, NoError> {
return postbox.transaction { transaction -> Api.InputPeer? in return postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
} }
|> mapToSignal { inputPeer -> Signal<EngineAudioTranscriptionResult?, NoError> in |> mapToSignal { inputPeer -> Signal<EngineAudioTranscriptionResult, NoError> in
guard let inputPeer = inputPeer else { guard let inputPeer = inputPeer else {
return .single(nil) return .single(.error)
} }
return network.request(Api.functions.messages.transcribeAudio(peer: inputPeer, msgId: messageId.id)) return network.request(Api.functions.messages.transcribeAudio(peer: inputPeer, msgId: messageId.id))
|> map(Optional.init) |> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.TranscribedAudio?, NoError> in |> `catch` { _ -> Signal<Api.messages.TranscribedAudio?, NoError> in
return .single(nil) return .single(nil)
} }
|> mapToSignal { result -> Signal<EngineAudioTranscriptionResult?, NoError> in |> mapToSignal { result -> Signal<EngineAudioTranscriptionResult, NoError> in
guard let result = result else { guard let result = result else {
return .single(nil) return .single(.error)
} }
switch result {
case let .transcribedAudio(transcriptionId, text): return postbox.transaction { transaction -> EngineAudioTranscriptionResult in
return .single(EngineAudioTranscriptionResult(id: transcriptionId, text: text)) switch result {
case let .transcribedAudio(transcriptionId, text):
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is AudioTranscriptionMessageAttribute) }
attributes.append(AudioTranscriptionMessageAttribute(id: transcriptionId, text: text))
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
return .success(EngineAudioTranscriptionResult.Success(id: transcriptionId, text: text))
}
} }
} }
} }
} }
func _internal_rateAudioTranscription(postbox: Postbox, network: Network, messageId: MessageId, id: Int64, isGood: Bool) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer = inputPeer else {
return .complete()
}
return network.request(Api.functions.messages.rateTranscribedAudio(peer: inputPeer, msgId: messageId.id, transcriptionId: id, good: isGood ? .boolTrue : .boolFalse))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
}

View File

@ -348,8 +348,6 @@ extension ChatListFilter {
} }
) )
) )
case .dialogFilterDefault:
preconditionFailure()
} }
} }
@ -486,8 +484,6 @@ private func requestChatListFilters(accountPeerId: PeerId, postbox: Postbox, net
} }
} }
} }
case .dialogFilterDefault:
break
} }
} }
return (filters, missingPeers, missingChats) return (filters, missingPeers, missingChats)

View File

@ -273,6 +273,8 @@ swift_library(
"//submodules/InAppPurchaseManager:InAppPurchaseManager", "//submodules/InAppPurchaseManager:InAppPurchaseManager",
"//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent:AudioTranscriptionButtonComponent", "//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent:AudioTranscriptionButtonComponent",
"//submodules/TelegramUI/Components/AudioWaveformComponent:AudioWaveformComponent", "//submodules/TelegramUI/Components/AudioWaveformComponent:AudioWaveformComponent",
"//submodules/TelegramUI/Components/EditableChatTextNode:EditableChatTextNode",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC", "//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC",
"//submodules/Media/LocalAudioTranscription:LocalAudioTranscription", "//submodules/Media/LocalAudioTranscription:LocalAudioTranscription",
] + select({ ] + select({

View File

@ -139,7 +139,7 @@ public final class AudioWaveformComponent: Component {
self.updateShimmer() self.updateShimmer()
} }
if let previousContents = previousContents, let contents = self.contents { if let previousContents = previousContents, CFGetTypeID(previousContents as CFTypeRef) == CGImage.typeID, (previousContents as! CGImage).width != Int(image.size.width * image.scale), let contents = self.contents {
self.animate(from: previousContents as AnyObject, to: contents as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.15) self.animate(from: previousContents as AnyObject, to: contents as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.15)
} }
} }
@ -317,7 +317,7 @@ public final class AudioWaveformComponent: Component {
func update(component: AudioWaveformComponent, availableSize: CGSize, transition: Transition) -> CGSize { func update(component: AudioWaveformComponent, availableSize: CGSize, transition: Transition) -> CGSize {
let size = CGSize(width: availableSize.width, height: availableSize.height) let size = CGSize(width: availableSize.width, height: availableSize.height)
if self.validSize != size || self.component?.samples != component.samples || self.component?.peak != component.peak { if self.validSize != size || self.component != component {
self.setNeedsDisplay() self.setNeedsDisplay()
} }
@ -464,8 +464,6 @@ public final class AudioWaveformComponent: Component {
} }
memset(adjustedSamplesMemory, 0, numSamples * 2) memset(adjustedSamplesMemory, 0, numSamples * 2)
var generateFakeSamples = false
var bins: [UInt16: Int] = [:] var bins: [UInt16: Int] = [:]
for i in 0 ..< maxReadSamples { for i in 0 ..< maxReadSamples {
let index = i * numSamples / maxReadSamples let index = i * numSamples / maxReadSamples
@ -491,27 +489,6 @@ public final class AudioWaveformComponent: Component {
} }
sortedSamples.sort { $0.1 > $1.1 } sortedSamples.sort { $0.1 > $1.1 }
let topSamples = sortedSamples.prefix(1)
let topCount = topSamples.map{ $0.1 }.reduce(.zero, +)
var topCountPercent: Float = 0.0
if bins.count > 0 {
topCountPercent = Float(topCount) / Float(totalCount)
}
if topCountPercent > 0.75 {
generateFakeSamples = true
}
if generateFakeSamples {
if maxSample < 10 {
maxSample = 20
}
for i in 0 ..< maxReadSamples {
let index = i * numSamples / maxReadSamples
adjustedSamples[index] = UInt16.random(in: 6...maxSample)
}
}
let invScale = 1.0 / max(1.0, CGFloat(maxSample)) let invScale = 1.0 / max(1.0, CGFloat(maxSample))
let commonRevealFraction = listViewAnimationCurveSystem(self.revealProgress) let commonRevealFraction = listViewAnimationCurveSystem(self.revealProgress)

View File

@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "EditableChatTextNode",
module_name = "EditableChatTextNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,7 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
public final class EditableChatTextNode: EditableTextNode {
}

View File

@ -0,0 +1,26 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "EmojiTextAttachmentView",
module_name = "EmojiTextAttachmentView",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/YuvConversion:YuvConversion",
"//submodules/AccountContext:AccountContext",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,213 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SwiftSignalKit
import AccountContext
import YuvConversion
import TelegramCore
import Postbox
private final class InlineStickerItemLayer: SimpleLayer {
static let queue = Queue()
private let file: TelegramMediaFile
private let source: AnimatedStickerNodeSource
private var frameSource: QueueLocalObject<AnimatedStickerDirectFrameSource>?
private var disposable: Disposable?
private var fetchDisposable: Disposable?
private var isInHierarchyValue: Bool = false
var isVisibleForAnimations: Bool = false {
didSet {
self.updatePlayback()
}
}
private var displayLink: ConstantDisplayLinkAnimator?
init(context: AccountContext, file: TelegramMediaFile) {
self.source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
self.file = file
super.init()
let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
let width = Int(24 * UIScreenScale)
let height = Int(24 * UIScreenScale)
let directDataPath = Atomic<String?>(value: nil)
let _ = (self.source.directDataPath(attemptSynchronously: true) |> take(1)).start(next: { result in
let _ = directDataPath.swap(result)
})
if let directDataPath = directDataPath.with({ $0 }), let directData = try? Data(contentsOf: URL(fileURLWithPath: directDataPath), options: .alwaysMapped) {
let syncFrameSource = AnimatedStickerDirectFrameSource(queue: .mainQueue(), data: directData, width: width, height: height, cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)!
if let animationFrame = syncFrameSource.takeFrame(draw: true) {
var image: UIImage?
autoreleasepool {
image = generateImagePixel(CGSize(width: CGFloat(animationFrame.width), height: CGFloat(animationFrame.height)), scale: 1.0, pixelGenerator: { _, pixelData, contextBytesPerRow in
var data = animationFrame.data
data.withUnsafeMutableBytes { bytes -> Void in
guard let baseAddress = bytes.baseAddress else {
return
}
switch animationFrame.type {
case .argb:
memcpy(pixelData, baseAddress.assumingMemoryBound(to: UInt8.self), bytes.count)
case .yuva:
if animationFrame.bytesPerRow <= 0 || animationFrame.height <= 0 || animationFrame.width <= 0 || animationFrame.bytesPerRow * animationFrame.height > bytes.count {
assert(false)
return
}
decodeYUVAToRGBA(baseAddress.assumingMemoryBound(to: UInt8.self), pixelData, Int32(animationFrame.width), Int32(animationFrame.height), Int32(contextBytesPerRow))
default:
break
}
}
})
}
if let image = image {
self.contents = image.cgImage
}
}
}
self.disposable = (self.source.directDataPath(attemptSynchronously: false)
|> filter { $0 != nil }
|> take(1)
|> deliverOn(InlineStickerItemLayer.queue)).start(next: { [weak self] path in
guard let directData = try? Data(contentsOf: URL(fileURLWithPath: path!), options: [.mappedRead]) else {
return
}
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.frameSource = QueueLocalObject(queue: InlineStickerItemLayer.queue, generate: {
return AnimatedStickerDirectFrameSource(queue: InlineStickerItemLayer.queue, data: directData, width: width, height: height, cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)!
})
strongSelf.updatePlayback()
}
})
self.fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()
}
override init(layer: Any) {
guard let layer = layer as? InlineStickerItemLayer else {
preconditionFailure()
}
self.source = layer.source
self.file = layer.file
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable?.dispose()
self.fetchDisposable?.dispose()
}
override func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchyValue = true
} else if event == kCAOnOrderOut {
self.isInHierarchyValue = false
}
self.updatePlayback()
return nullAction
}
private func updatePlayback() {
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations && self.frameSource != nil
if shouldBePlaying != (self.displayLink != nil) {
if shouldBePlaying {
self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.loadNextFrame()
})
self.displayLink?.isPaused = false
} else {
self.displayLink?.invalidate()
self.displayLink = nil
}
}
}
private var didRequestFrame = false
private func loadNextFrame() {
guard let frameSource = self.frameSource else {
return
}
self.didRequestFrame = true
frameSource.with { [weak self] impl in
if let animationFrame = impl.takeFrame(draw: true) {
var image: UIImage?
autoreleasepool {
image = generateImagePixel(CGSize(width: CGFloat(animationFrame.width), height: CGFloat(animationFrame.height)), scale: 1.0, pixelGenerator: { _, pixelData, contextBytesPerRow in
var data = animationFrame.data
data.withUnsafeMutableBytes { bytes -> Void in
guard let baseAddress = bytes.baseAddress else {
return
}
switch animationFrame.type {
case .argb:
memcpy(pixelData, baseAddress.assumingMemoryBound(to: UInt8.self), bytes.count)
case .yuva:
if animationFrame.bytesPerRow <= 0 || animationFrame.height <= 0 || animationFrame.width <= 0 || animationFrame.bytesPerRow * animationFrame.height > bytes.count {
assert(false)
return
}
decodeYUVAToRGBA(baseAddress.assumingMemoryBound(to: UInt8.self), pixelData, Int32(animationFrame.width), Int32(animationFrame.height), Int32(contextBytesPerRow))
default:
break
}
}
})
}
if let image = image {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.contents = image.cgImage
}
}
}
}
}
}
public final class EmojiTextAttachmentView: UIView {
private let contentLayer: InlineStickerItemLayer
public init(context: AccountContext, file: TelegramMediaFile) {
self.contentLayer = InlineStickerItemLayer(context: context, file: file)
super.init(frame: CGRect())
self.layer.addSublayer(self.contentLayer)
self.contentLayer.isVisibleForAnimations = true
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func layoutSubviews() {
super.layoutSubviews()
self.contentLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -2.0), size: CGSize(width: self.bounds.width - 0.0, height: self.bounds.height + 9.0))
}
}

View File

@ -159,12 +159,20 @@ public final class AccountContextImpl: AccountContext {
public let cachedGroupCallContexts: AccountGroupCallContextCache public let cachedGroupCallContexts: AccountGroupCallContextCache
public let meshAnimationCache: MeshAnimationCache public let meshAnimationCache: MeshAnimationCache
private var animatedEmojiStickersDisposable: Disposable?
public private(set) var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
private var userLimitsConfigurationDisposable: Disposable?
public private(set) var userLimits: EngineConfiguration.UserLimits
public init(sharedContext: SharedAccountContextImpl, account: Account, limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, temp: Bool = false) public init(sharedContext: SharedAccountContextImpl, account: Account, limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, temp: Bool = false)
{ {
self.sharedContextImpl = sharedContext self.sharedContextImpl = sharedContext
self.account = account self.account = account
self.engine = TelegramEngine(account: account) self.engine = TelegramEngine(account: account)
self.userLimits = EngineConfiguration.UserLimits(UserLimitsConfiguration.defaultValue)
self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager) self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager)
if let locationManager = self.sharedContextImpl.locationManager { if let locationManager = self.sharedContextImpl.locationManager {
@ -244,6 +252,40 @@ public final class AccountContextImpl: AccountContext {
account.callSessionManager.updateVersions(versions: PresentationCallManagerImpl.voipVersions(includeExperimental: true, includeReference: true).map { version, supportsVideo -> CallSessionManagerImplementationVersion in account.callSessionManager.updateVersions(versions: PresentationCallManagerImpl.voipVersions(includeExperimental: true, includeReference: true).map { version, supportsVideo -> CallSessionManagerImplementationVersion in
CallSessionManagerImplementationVersion(version: version, supportsVideo: supportsVideo) CallSessionManagerImplementationVersion(version: version, supportsVideo: supportsVideo)
}) })
self.animatedEmojiStickersDisposable = (self.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false)
|> map { animatedEmoji -> [String: [StickerPackItem]] in
var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
switch animatedEmoji {
case let .result(_, items, _):
for item in items {
if let emoji = item.getStringRepresentationsOfIndexKeys().first {
animatedEmojiStickers[emoji.basicEmoji.0] = [item]
let strippedEmoji = emoji.basicEmoji.0.strippedEmoji
if animatedEmojiStickers[strippedEmoji] == nil {
animatedEmojiStickers[strippedEmoji] = [item]
}
}
}
default:
break
}
return animatedEmojiStickers
}
|> deliverOnMainQueue).start(next: { [weak self] stickers in
guard let strongSelf = self else {
return
}
strongSelf.animatedEmojiStickers = stickers
})
self.userLimitsConfigurationDisposable = (self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false))
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.userLimits = value
})
} }
deinit { deinit {
@ -252,6 +294,7 @@ public final class AccountContextImpl: AccountContext {
self.contentSettingsDisposable?.dispose() self.contentSettingsDisposable?.dispose()
self.appConfigurationDisposable?.dispose() self.appConfigurationDisposable?.dispose()
self.experimentalUISettingsDisposable?.dispose() self.experimentalUISettingsDisposable?.dispose()
self.animatedEmojiStickersDisposable?.dispose()
} }
public func storeSecureIdPassword(password: String) { public func storeSecureIdPassword(password: String) {

View File

@ -138,6 +138,7 @@ public final class ChatControllerInteraction {
var canPlayMedia: Bool = false var canPlayMedia: Bool = false
var hiddenMedia: [MessageId: [Media]] = [:] var hiddenMedia: [MessageId: [Media]] = [:]
var expandedTranslationMessageStableIds: Set<UInt32> = Set()
var selectionState: ChatInterfaceSelectionState? var selectionState: ChatInterfaceSelectionState?
var highlightedState: ChatInterfaceHighlightedState? var highlightedState: ChatInterfaceHighlightedState?
var contextHighlightedState: ChatInterfaceHighlightedState? var contextHighlightedState: ChatInterfaceHighlightedState?

View File

@ -1932,9 +1932,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
case .none: case .none:
break break
case .inputButtons: case .inputButtons:
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in if let peer = self.chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil {
return (.none, state.keyboardButtonsMessage?.id ?? state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) } else {
}) self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
return (.none, state.keyboardButtonsMessage?.id ?? state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
})
}
default: default:
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
return (.none, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) return (.none, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
@ -2427,7 +2430,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
for text in breakChatInputText(trimChatInputText(inputText)) { for text in breakChatInputText(trimChatInputText(inputText)) {
if text.length != 0 { if text.length != 0 {
var attributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = []
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: Int(self.context.userLimits.maxAnimatedEmojisInText)))
if !entities.isEmpty { if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities)) attributes.append(TextEntitiesMessageAttribute(entities: entities))
} }

View File

@ -684,6 +684,25 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
} }
} }
var audioTranscription: AudioTranscriptionMessageAttribute?
for attribute in message.attributes {
if let attribute = attribute as? AudioTranscriptionMessageAttribute {
audioTranscription = attribute
break
}
}
if let audioTranscription = audioTranscription {
actions.insert(.custom(ChatRateTranscriptionContextItem(context: context, message: message, action: { [weak context] value in
guard let context = context else {
return
}
let _ = context.engine.messages.rateAudioTranscription(messageId: message.id, id: audioTranscription.id, isGood: value).start()
}), false), at: 0)
actions.insert(.separator, at: 1)
}
for media in message.media { for media in message.media {
if let file = media as? TelegramMediaFile, let size = file.size, size < 1 * 1024 * 1024, let duration = file.duration, duration < 60, (["audio/mpeg", "audio/mp3", "audio/mpeg3", "audio/ogg"] as [String]).contains(file.mimeType.lowercased()) { if let file = media as? TelegramMediaFile, let size = file.size, size < 1 * 1024 * 1024, let duration = file.duration, duration < 60, (["audio/mpeg", "audio/mp3", "audio/mpeg3", "audio/ogg"] as [String]).contains(file.mimeType.lowercased()) {
let fileName = file.fileName ?? "Tone" let fileName = file.fileName ?? "Tone"
@ -716,9 +735,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}) })
} }
}))) })))
actions.append(.separator)
} }
} }
actions.append(.separator)
} }
var isReplyThreadHead = false var isReplyThreadHead = false
@ -773,6 +792,16 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
} }
} }
for attribute in message.attributes {
if let attribute = attribute as? AudioTranscriptionMessageAttribute {
if !messageText.isEmpty {
messageText.append("\n")
}
messageText.append(attribute.text)
break
}
}
var isPoll = false var isPoll = false
if messageText.isEmpty { if messageText.isEmpty {
for media in message.media { for media in message.media {
@ -2293,3 +2322,162 @@ private func stringForRemainingTime(_ duration: Int32, strings: PresentationStri
} }
return strings.Conversation_AutoremoveRemainingTime(durationString).string return strings.Conversation_AutoremoveRemainingTime(durationString).string
} }
final class ChatRateTranscriptionContextItem: ContextMenuCustomItem {
fileprivate let context: AccountContext
fileprivate let message: Message
fileprivate let action: (Bool) -> Void
init(context: AccountContext, message: Message, action: @escaping (Bool) -> Void) {
self.context = context
self.message = message
self.action = action
}
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return ChatRateTranscriptionContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected, action: self.action)
}
}
private final class ChatRateTranscriptionContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol {
private let item: ChatRateTranscriptionContextItem
private var presentationData: PresentationData
private let getController: () -> ContextControllerProtocol?
private let actionSelected: (ContextMenuActionResult) -> Void
private let action: (Bool) -> Void
private let backgroundNode: ASDisplayNode
private let textNode: ImmediateTextNode
private let upButtonImageNode: ASImageNode
private let downButtonImageNode: ASImageNode
private let upButtonNode: HighlightableButtonNode
private let downButtonNode: HighlightableButtonNode
init(presentationData: PresentationData, item: ChatRateTranscriptionContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, action: @escaping (Bool) -> Void) {
self.item = item
self.presentationData = presentationData
self.getController = getController
self.actionSelected = actionSelected
self.action = action
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0)
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isAccessibilityElement = false
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
self.textNode = ImmediateTextNode()
self.textNode.isAccessibilityElement = false
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = NSAttributedString(string: "Rate Transcription", font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor)
self.textNode.maximumNumberOfLines = 1
self.upButtonImageNode = ASImageNode()
self.upButtonImageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DarkMode"), color: presentationData.theme.contextMenu.primaryColor, backgroundColor: nil)
self.upButtonImageNode.isUserInteractionEnabled = false
self.downButtonImageNode = ASImageNode()
self.downButtonImageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: presentationData.theme.contextMenu.primaryColor, backgroundColor: nil)
self.downButtonImageNode.isUserInteractionEnabled = false
self.upButtonNode = HighlightableButtonNode()
self.upButtonNode.addSubnode(self.upButtonImageNode)
self.downButtonNode = HighlightableButtonNode()
self.downButtonNode.addSubnode(self.downButtonImageNode)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
self.addSubnode(self.upButtonNode)
self.addSubnode(self.downButtonNode)
self.upButtonNode.addTarget(self, action: #selector(self.upPressed), forControlEvents: .touchUpInside)
self.downButtonNode.addTarget(self, action: #selector(self.downPressed), forControlEvents: .touchUpInside)
}
deinit {
}
override func didLoad() {
super.didLoad()
}
@objc private func upPressed() {
self.action(true)
self.getController()?.dismiss(completion: nil)
}
@objc private func downPressed() {
self.action(false)
self.getController()?.dismiss(completion: nil)
}
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let sideInset: CGFloat = 14.0
let verticalInset: CGFloat = 9.0
let calculatedWidth = min(constrainedWidth, 250.0)
let textSize = self.textNode.updateLayout(CGSize(width: calculatedWidth - sideInset, height: .greatestFiniteMagnitude))
let combinedTextHeight = textSize.height
return (CGSize(width: calculatedWidth, height: verticalInset * 2.0 + combinedTextHeight + 35.0), { size, transition in
let verticalOrigin = verticalInset
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: verticalOrigin), size: textSize)
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
let buttonArea = CGRect(origin: CGPoint(x: 0.0, y: size.height - 35.0 - 6.0), size: CGSize(width: size.width, height: 35.0))
self.upButtonNode.frame = CGRect(origin: CGPoint(x: buttonArea.minX, y: buttonArea.minY), size: CGSize(width: floor(buttonArea.size.width / 2.0), height: buttonArea.height))
self.downButtonNode.frame = CGRect(origin: CGPoint(x: buttonArea.minX + floor(buttonArea.size.width / 2.0), y: buttonArea.minY), size: CGSize(width: floor(buttonArea.size.width / 2.0), height: buttonArea.height))
let spacing: CGFloat = 56.0
if let image = self.upButtonImageNode.image {
self.upButtonImageNode.frame = CGRect(origin: CGPoint(x: floor(buttonArea.width / 2.0) - floor(spacing / 2.0) - image.size.width, y: floor((buttonArea.height - image.size.height) / 2.0)), size: image.size)
}
if let image = self.downButtonImageNode.image {
self.downButtonImageNode.frame = CGRect(origin: CGPoint(x: floor(spacing / 2.0), y: floor((buttonArea.height - image.size.height) / 2.0)), size: image.size)
}
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
})
}
func updateTheme(presentationData: PresentationData) {
self.presentationData = presentationData
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
}
func canBeHighlighted() -> Bool {
return self.isActionEnabled
}
func updateIsHighlighted(isHighlighted: Bool) {
self.setIsHighlighted(isHighlighted)
}
func performAction() {
}
var isActionEnabled: Bool {
return false
}
func setIsHighlighted(_ value: Bool) {
}
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
return self
}
}

View File

@ -175,7 +175,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var fileIconImage: UIImage? private var fileIconImage: UIImage?
private var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .possible private var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .possible
private var transcribedText: String? private var transcribedText: EngineAudioTranscriptionResult?
private var transcribeDisposable: Disposable? private var transcribeDisposable: Disposable?
override init() { override init() {
@ -305,6 +305,17 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
guard let context = self.context, let message = self.message, let presentationData = self.presentationData else { guard let context = self.context, let message = self.message, let presentationData = self.presentationData else {
return return
} }
if self.transcribedText == nil {
for attribute in message.attributes {
if let attribute = attribute as? AudioTranscriptionMessageAttribute {
self.transcribedText = .success(EngineAudioTranscriptionResult.Success(id: attribute.id, text: attribute.text))
self.audioTranscriptionState = .collapsed
break
}
}
}
if self.transcribedText == nil { if self.transcribedText == nil {
if self.transcribeDisposable == nil { if self.transcribeDisposable == nil {
self.audioTranscriptionState = .inProgress self.audioTranscriptionState = .inProgress
@ -352,7 +363,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
return return
} }
strongSelf.transcribeDisposable = nil strongSelf.transcribeDisposable = nil
strongSelf.transcribedText = result if let result = result {
strongSelf.transcribedText = .success(EngineAudioTranscriptionResult.Success(id: 0, text: result))
} else {
strongSelf.transcribedText = .error
}
if strongSelf.transcribedText != nil { if strongSelf.transcribedText != nil {
strongSelf.audioTranscriptionState = .expanded strongSelf.audioTranscriptionState = .expanded
} else { } else {
@ -368,7 +383,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
} }
strongSelf.transcribeDisposable = nil strongSelf.transcribeDisposable = nil
strongSelf.audioTranscriptionState = .expanded strongSelf.audioTranscriptionState = .expanded
strongSelf.transcribedText = result?.text strongSelf.transcribedText = result
strongSelf.requestUpdateLayout(true) strongSelf.requestUpdateLayout(true)
}) })
} }
@ -573,7 +588,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
let textFont = arguments.presentationData.messageFont let textFont = arguments.presentationData.messageFont
let textString: NSAttributedString? let textString: NSAttributedString?
if let transcribedText = transcribedText, case .expanded = audioTranscriptionState { if let transcribedText = transcribedText, case .expanded = audioTranscriptionState {
textString = NSAttributedString(string: transcribedText, font: textFont, textColor: messageTheme.primaryTextColor) switch transcribedText {
case let .success(success):
textString = NSAttributedString(string: success.text, font: textFont, textColor: messageTheme.primaryTextColor)
case .error:
let errorTextFont = Font.regular(floor(arguments.presentationData.fontSize.baseDisplaySize * 15.0 / 17.0))
//TODO:localize
textString = NSAttributedString(string: "No speech detected", font: errorTextFont, textColor: messageTheme.secondaryTextColor)
}
} else { } else {
textString = nil textString = nil
} }

View File

@ -91,10 +91,11 @@ private final class InlineStickerItemLayer: SimpleLayer {
let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
self.disposable = (self.source.directDataPath() self.disposable = (self.source.directDataPath(attemptSynchronously: false)
|> filter { $0 != nil }
|> take(1) |> take(1)
|> deliverOn(InlineStickerItemLayer.queue)).start(next: { [weak self] path in |> deliverOn(InlineStickerItemLayer.queue)).start(next: { [weak self] path in
guard let directData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) else { guard let directData = try? Data(contentsOf: URL(fileURLWithPath: path!), options: [.mappedRead]) else {
return return
} }
Queue.mainQueue().async { Queue.mainQueue().async {
@ -161,12 +162,6 @@ private final class InlineStickerItemLayer: SimpleLayer {
guard let frameSource = self.frameSource else { guard let frameSource = self.frameSource else {
return return
} }
if self.contents != nil {
return
}
if self.didRequestFrame {
return
}
self.didRequestFrame = true self.didRequestFrame = true
frameSource.with { [weak self] impl in frameSource.with { [weak self] impl in
if let animationFrame = impl.takeFrame(draw: true) { if let animationFrame = impl.takeFrame(draw: true) {
@ -200,9 +195,6 @@ private final class InlineStickerItemLayer: SimpleLayer {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
if strongSelf.contents != nil {
return
}
strongSelf.contents = image.cgImage strongSelf.contents = image.cgImage
} }
} }
@ -473,41 +465,72 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
attributedText = NSAttributedString(string: " ", font: textFont, textColor: messageTheme.primaryTextColor) attributedText = NSAttributedString(string: " ", font: textFont, textColor: messageTheme.primaryTextColor)
} }
/*if item.context.sharedContext.immediateExperimentalUISettings.inlineStickers*/ do { if let entities = entities {
var currentCount = 0
let updatedString = NSMutableAttributedString(attributedString: attributedText) let updatedString = NSMutableAttributedString(attributedString: attributedText)
var startIndex = updatedString.string.startIndex
while true { for entity in entities.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) {
var hadUpdates = false guard case .AnimatedEmoji = entity.type else {
updatedString.string.enumerateSubstrings(in: startIndex ..< updatedString.string.endIndex, options: [.byComposedCharacterSequences]) { substring, substringRange, _, stop in continue
if let substring = substring { }
let emoji = substring.basicEmoji.0
let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
var emojiFile: TelegramMediaFile?
emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file let substring = updatedString.attributedSubstring(from: range)
if emojiFile == nil {
emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file let emoji = substring.string.basicEmoji.0
}
var emojiFile: TelegramMediaFile?
if let emojiFile = emojiFile { emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
let currentDict = updatedString.attributes(at: NSRange(substringRange, in: updatedString.string).lowerBound, effectiveRange: nil) if emojiFile == nil {
var updatedAttributes: [NSAttributedString.Key: Any] = currentDict emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor }
updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(file: emojiFile)
if let emojiFile = emojiFile {
let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil)
var updatedAttributes: [NSAttributedString.Key: Any] = currentDict
updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor
updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(file: emojiFile)
let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}\u{00a0}]", attributes: updatedAttributes)
//updatedString.insert(insertString, at: NSRange(substringRange, in: updatedString.string).upperBound)
updatedString.replaceCharacters(in: range, with: insertString)
}
/*var currentCount = 0
let updatedString = NSMutableAttributedString(attributedString: attributedText)
var startIndex = updatedString.string.startIndex
while true {
var hadUpdates = false
updatedString.string.enumerateSubstrings(in: startIndex ..< updatedString.string.endIndex, options: [.byComposedCharacterSequences]) { substring, substringRange, _, stop in
if let substring = substring {
let emoji = substring.basicEmoji.0
let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}\u{00a0}]", attributes: updatedAttributes) var emojiFile: TelegramMediaFile?
//updatedString.insert(insertString, at: NSRange(substringRange, in: updatedString.string).upperBound) emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
updatedString.replaceCharacters(in: NSRange(substringRange, in: updatedString.string), with: insertString) if emojiFile == nil {
startIndex = substringRange.lowerBound emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
currentCount += 1 }
hadUpdates = true
stop = true if let emojiFile = emojiFile {
let currentDict = updatedString.attributes(at: NSRange(substringRange, in: updatedString.string).lowerBound, effectiveRange: nil)
var updatedAttributes: [NSAttributedString.Key: Any] = currentDict
updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor
updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(file: emojiFile)
let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}\u{00a0}]", attributes: updatedAttributes)
//updatedString.insert(insertString, at: NSRange(substringRange, in: updatedString.string).upperBound)
updatedString.replaceCharacters(in: NSRange(substringRange, in: updatedString.string), with: insertString)
startIndex = substringRange.lowerBound
currentCount += 1
hadUpdates = true
stop = true
}
} }
} }
} if !hadUpdates || currentCount >= 10 {
if !hadUpdates || currentCount >= 10 { break
break }
} }*/
} }
attributedText = updatedString attributedText = updatedString
} }

View File

@ -23,6 +23,8 @@ import Pasteboard
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import ManagedAnimationNode import ManagedAnimationNode
import AttachmentUI import AttachmentUI
import EditableChatTextNode
import EmojiTextAttachmentView
private let accessoryButtonFont = Font.medium(14.0) private let accessoryButtonFont = Font.medium(14.0)
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
@ -409,8 +411,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
} }
textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider)
textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count)
if let presentationInterfaceState = self.presentationInterfaceState {
refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider)
}
self.updatingInputState = false self.updatingInputState = false
self.keepSendButtonEnabled = keepSendButtonEnabled self.keepSendButtonEnabled = keepSendButtonEnabled
self.extendedSearchLayout = extendedSearchLayout self.extendedSearchLayout = extendedSearchLayout
@ -452,6 +459,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
private var touchDownGestureRecognizer: TouchDownGestureRecognizer? private var touchDownGestureRecognizer: TouchDownGestureRecognizer?
private var emojiViewProvider: ((String) -> UIView)?
init(presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { init(presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
self.presentationInterfaceState = presentationInterfaceState self.presentationInterfaceState = presentationInterfaceState
@ -657,6 +666,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
} }
self.textInputBackgroundNode.isUserInteractionEnabled = true self.textInputBackgroundNode.isUserInteractionEnabled = true
self.textInputBackgroundNode.view.addGestureRecognizer(recognizer) self.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
self.emojiViewProvider = { [weak self] emoji in
guard let strongSelf = self, let context = strongSelf.context, let file = strongSelf.context?.animatedEmojiStickers[emoji]?.first?.file else {
return UIView()
}
return EmojiTextAttachmentView(context: context, file: file)
}
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
@ -674,7 +691,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
} }
private func loadTextInputNode() { private func loadTextInputNode() {
let textInputNode = EditableTextNode() let textInputNode = EditableChatTextNode()
textInputNode.initialPrimaryLanguage = self.presentationInterfaceState?.interfaceState.inputLanguage textInputNode.initialPrimaryLanguage = self.presentationInterfaceState?.interfaceState.inputLanguage
var textColor: UIColor = .black var textColor: UIColor = .black
var tintColor: UIColor = .blue var tintColor: UIColor = .blue
@ -1858,7 +1875,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState {
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed) refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider)
refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
self.updateSpoiler() self.updateSpoiler()
@ -1983,9 +2000,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
textInputNode.textView.isScrollEnabled = false textInputNode.textView.isScrollEnabled = false
refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed) refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider)
textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider)
if textInputNode.textView.subviews.count > 1, animated { if textInputNode.textView.subviews.count > 1, animated {
let containerView = textInputNode.textView.subviews[1] let containerView = textInputNode.textView.subviews[1]
@ -2310,6 +2327,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage
self.inputMenu.deactivate() self.inputMenu.deactivate()
if let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil {
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { _ in
return (.inputButtons, nil)
}
}
} }
func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? { func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? {
@ -2487,7 +2510,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
} }
let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider)
string.replaceCharacters(in: range, with: cleanReplacementString) string.replaceCharacters(in: range, with: cleanReplacementString)
self.textInputNode?.attributedText = string self.textInputNode?.attributedText = string
self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0) self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0)

View File

@ -2929,7 +2929,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|> `catch` { _ -> Signal<Bool, NoError> in |> `catch` { _ -> Signal<Bool, NoError> in
return .single(false) return .single(false)
})) }))
#if DEBUG
#else
self.cachedFaq.set(.single(nil) |> then(cachedFaqInstantPage(context: self.context) |> map(Optional.init))) self.cachedFaq.set(.single(nil) |> then(cachedFaqInstantPage(context: self.context) |> map(Optional.init)))
#endif
screenData = peerInfoScreenSettingsData(context: context, peerId: peerId, accountsAndPeers: self.accountsAndPeers.get(), activeSessionsContextAndCount: self.activeSessionsContextAndCount.get(), notificationExceptions: self.notificationExceptions.get(), privacySettings: self.privacySettings.get(), archivedStickerPacks: self.archivedPacks.get(), hasPassport: self.hasPassport.get()) screenData = peerInfoScreenSettingsData(context: context, peerId: peerId, accountsAndPeers: self.accountsAndPeers.get(), activeSessionsContextAndCount: self.activeSessionsContextAndCount.get(), notificationExceptions: self.notificationExceptions.get(), privacySettings: self.privacySettings.get(), archivedStickerPacks: self.archivedPacks.get(), hasPassport: self.hasPassport.get())

View File

@ -14,6 +14,7 @@ swift_library(
"//submodules/Display:Display", "//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/Markdown:Markdown", "//submodules/Markdown:Markdown",
"//submodules/Emoji:Emoji",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -4,6 +4,7 @@ import Display
import AsyncDisplayKit import AsyncDisplayKit
import Postbox import Postbox
import TelegramPresentationData import TelegramPresentationData
import Emoji
private let alphanumericCharacters = CharacterSet.alphanumerics private let alphanumericCharacters = CharacterSet.alphanumerics
@ -21,12 +22,28 @@ public struct ChatTextInputAttributes {
} }
public func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttributedString { public func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttributedString {
let result = NSMutableAttributedString(string: text.string) let sourceString = NSMutableAttributedString(attributedString: text)
while true {
var found = false
let fullRange = NSRange(sourceString.string.startIndex ..< sourceString.string.endIndex, in: sourceString.string)
sourceString.enumerateAttribute(NSAttributedString.Key.attachment, in: fullRange, options: [.longestEffectiveRangeNotRequired], using: { value, range, stop in
if let value = value as? EmojiTextAttachment {
sourceString.replaceCharacters(in: range, with: NSAttributedString(string: value.emoji))
stop.pointee = true
found = true
}
})
if !found {
break
}
}
let result = NSMutableAttributedString(string: sourceString.string)
let fullRange = NSRange(location: 0, length: result.length) let fullRange = NSRange(location: 0, length: result.length)
text.enumerateAttributes(in: fullRange, options: [], using: { attributes, range, _ in sourceString.enumerateAttributes(in: fullRange, options: [], using: { attributes, range, _ in
for (key, value) in attributes { for (key, value) in attributes {
if ChatTextInputAttributes.allAttributes.contains(key) { if ChatTextInputAttributes.allAttributes.contains(key) || key == NSAttributedString.Key.attachment {
result.addAttribute(key, value: value, range: range) result.addAttribute(key, value: value, range: range)
} }
} }
@ -47,7 +64,7 @@ public struct ChatTextFontAttributes: OptionSet {
public static let blockQuote = ChatTextFontAttributes(rawValue: 1 << 3) public static let blockQuote = ChatTextFontAttributes(rawValue: 1 << 3)
} }
public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?, spoilersRevealed: Bool) -> NSAttributedString { public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?, spoilersRevealed: Bool, availableEmojis: Set<String>, emojiViewProvider: ((String) -> UIView)?) -> NSAttributedString {
let result = NSMutableAttributedString(string: stateText.string) let result = NSMutableAttributedString(string: stateText.string)
let fullRange = NSRange(location: 0, length: result.length) let fullRange = NSRange(location: 0, length: result.length)
@ -117,6 +134,36 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo
} }
} }
}) })
/*if #available(iOS 15, *), let emojiViewProvider = emojiViewProvider {
let _ = CustomTextAttachmentViewProvider.ensureRegistered
var nextIndex: [String: Int] = [:]
result.string.enumerateSubstrings(in: result.string.startIndex ..< result.string.endIndex, options: [.byComposedCharacterSequences]) { substring, substringRange, _, stop in
if let substring = substring {
let emoji = substring.basicEmoji.0
if !emoji.isEmpty && emoji.isSingleEmoji && availableEmojis.contains(emoji) {
let index: Int
if let value = nextIndex[emoji] {
index = value
} else {
index = 0
}
nextIndex[emoji] = index + 1
let attachment = EmojiTextAttachment(index: index, emoji: emoji, viewProvider: emojiViewProvider)
attachment.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 16.0))
result.replaceCharacters(in: NSRange(substringRange, in: result.string), with: NSAttributedString(attachment: attachment))
stop = true
}
}
}
}*/
return result return result
} }
@ -412,7 +459,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed
} }
} }
public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool) { public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set<String>, emojiViewProvider: ((String) -> UIView)?) {
guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else { guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else {
return return
} }
@ -427,16 +474,18 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme
var attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(initialAttributedText)) var attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(initialAttributedText))
refreshTextMentions(text: text, initialAttributedText: initialAttributedText, attributedText: attributedText, fullRange: fullRange) refreshTextMentions(text: text, initialAttributedText: initialAttributedText, attributedText: attributedText, fullRange: fullRange)
var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed) var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider)
text = resultAttributedText.string as NSString text = resultAttributedText.string as NSString
fullRange = NSRange(location: 0, length: initialAttributedText.length) fullRange = NSRange(location: 0, length: text.length)
attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText)) attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText))
refreshTextUrls(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange) refreshTextUrls(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange)
resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed) resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider)
if !resultAttributedText.isEqual(to: initialAttributedText) { if !resultAttributedText.isEqual(to: initialAttributedText) {
fullRange = NSRange(location: 0, length: textNode.textView.textStorage.length)
textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange) textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange)
textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.foregroundColor, range: fullRange) textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.foregroundColor, range: fullRange)
textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.underlineStyle, range: fullRange) textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.underlineStyle, range: fullRange)
@ -508,9 +557,55 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme
} }
}) })
} }
if #available(iOS 15, *), let emojiViewProvider = emojiViewProvider {
let _ = CustomTextAttachmentViewProvider.ensureRegistered
var nextIndex: [String: Int] = [:]
var count = 0
let fullRange = NSRange(textNode.textView.textStorage.string.startIndex ..< textNode.textView.textStorage.string.endIndex, in: textNode.textView.textStorage.string)
textNode.textView.textStorage.enumerateAttribute(NSAttributedString.Key.attachment, in: fullRange, options: [], using: { value, _, _ in
if let _ = value as? EmojiTextAttachment {
count += 1
}
})
while count < 10 {
var found = false
textNode.textView.textStorage.string.enumerateSubstrings(in: textNode.textView.textStorage.string.startIndex ..< textNode.textView.textStorage.string.endIndex, options: [.byComposedCharacterSequences]) { substring, substringRange, _, stop in
if let substring = substring {
let emoji = substring.basicEmoji.0
if !emoji.isEmpty && emoji.isSingleEmoji && availableEmojis.contains(emoji) {
let index: Int
if let value = nextIndex[emoji] {
index = value
} else {
index = 0
}
nextIndex[emoji] = index + 1
let attachment = EmojiTextAttachment(index: index, emoji: emoji, viewProvider: emojiViewProvider)
attachment.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 16.0))
textNode.textView.textStorage.replaceCharacters(in: NSRange(substringRange, in: textNode.textView.textStorage.string), with: NSAttributedString(attachment: attachment))
count += 1
found = true
stop = true
}
}
}
if !found {
break
}
}
}
} }
public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool = false) { public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set<String>, emojiViewProvider: ((String) -> UIView)?, spoilersRevealed: Bool = false) {
guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else { guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else {
return return
} }
@ -523,14 +618,14 @@ public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, th
var text: NSString = initialAttributedText.string as NSString var text: NSString = initialAttributedText.string as NSString
var fullRange = NSRange(location: 0, length: initialAttributedText.length) var fullRange = NSRange(location: 0, length: initialAttributedText.length)
var attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(initialAttributedText)) var attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(initialAttributedText))
var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed) var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider)
text = resultAttributedText.string as NSString text = resultAttributedText.string as NSString
fullRange = NSRange(location: 0, length: initialAttributedText.length) fullRange = NSRange(location: 0, length: initialAttributedText.length)
attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText)) attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText))
refreshTextUrls(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange) refreshTextUrls(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange)
resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed) resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider)
if !resultAttributedText.isEqual(to: initialAttributedText) { if !resultAttributedText.isEqual(to: initialAttributedText) {
textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange) textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange)
@ -805,3 +900,39 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu
return text return text
} }
private final class EmojiTextAttachment: NSTextAttachment {
let emoji: String
let viewProvider: (String) -> UIView
init(index: Int, emoji: String, viewProvider: @escaping (String) -> UIView) {
self.emoji = emoji
self.viewProvider = viewProvider
super.init(data: "\(emoji):\(index)".data(using: .utf8)!, ofType: "public.data")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@available(iOS 15, *)
private final class CustomTextAttachmentViewProvider: NSTextAttachmentViewProvider {
static let ensureRegistered: Bool = {
NSTextAttachment.registerViewProviderClass(CustomTextAttachmentViewProvider.self, forFileType: "public.data")
return true
}()
override func loadView() {
super.loadView()
if let attachment = self.textAttachment as? EmojiTextAttachment {
self.view = attachment.viewProvider(attachment.emoji)
} else {
self.view = UIView()
self.view!.backgroundColor = .clear
}
}
}

View File

@ -1,6 +1,7 @@
import Foundation import Foundation
import UIKit import UIKit
import TelegramCore import TelegramCore
import Emoji
private let whitelistedHosts: Set<String> = Set([ private let whitelistedHosts: Set<String> = Set([
"telegram.org", "telegram.org",
@ -142,8 +143,29 @@ private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType,
} }
} }
public func generateChatInputTextEntities(_ text: NSAttributedString) -> [MessageTextEntity] { public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimatedEmojisInText: Int? = nil) -> [MessageTextEntity] {
var entities: [MessageTextEntity] = [] var entities: [MessageTextEntity] = []
if let maxAnimatedEmojisInText = maxAnimatedEmojisInText {
var count = 0
text.string.enumerateSubstrings(in: text.string.startIndex ..< text.string.endIndex, options: [.byComposedCharacterSequences], { substring, substringRange, _, stop in
if let substring = substring {
let emoji = substring.basicEmoji.0
if !emoji.isEmpty && emoji.isSingleEmoji {
let mappedRange = NSRange(substringRange, in: text.string)
entities.append(MessageTextEntity(range: mappedRange.lowerBound ..< mappedRange.upperBound, type: .AnimatedEmoji))
count += 1
if count >= maxAnimatedEmojisInText {
stop = true
}
}
}
})
}
text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: [], using: { attributes, range, _ in text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: [], using: { attributes, range, _ in
for (key, value) in attributes { for (key, value) in attributes {
if key == ChatTextInputAttributes.bold { if key == ChatTextInputAttributes.bold {