mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Animated emoji
This commit is contained in:
parent
352916f866
commit
274f05d80a
@ -822,6 +822,10 @@ public protocol AccountContext: AnyObject {
|
||||
var cachedGroupCallContexts: AccountGroupCallContextCache { get }
|
||||
var meshAnimationCache: MeshAnimationCache { get }
|
||||
|
||||
var animatedEmojiStickers: [String: [StickerPackItem]] { get }
|
||||
|
||||
var userLimits: EngineConfiguration.UserLimits { get }
|
||||
|
||||
func storeSecureIdPassword(password: String)
|
||||
func getStoredSecureIdPassword() -> String?
|
||||
|
||||
|
@ -144,7 +144,7 @@ public protocol AnimatedStickerNodeSource {
|
||||
var isVideo: Bool { get }
|
||||
|
||||
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 {
|
||||
@ -304,9 +304,10 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
strongSelf.play(firstFrame: true)
|
||||
}
|
||||
}
|
||||
self.disposable.set((source.directDataPath()
|
||||
self.disposable.set((source.directDataPath(attemptSynchronously: false)
|
||||
|> filter { $0 != nil }
|
||||
|> deliverOnMainQueue).start(next: { path in
|
||||
f(path)
|
||||
f(path!)
|
||||
}))
|
||||
case .cached:
|
||||
self.disposable.set((source.cachedDataPath(width: width, height: height)
|
||||
|
@ -13,6 +13,27 @@
|
||||
|
||||
#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 () {
|
||||
// Prevent UITextView from updating contentOffset while deallocating: https://github.com/TextureGroup/Texture/issues/860
|
||||
BOOL _deallocating;
|
||||
@ -83,7 +104,7 @@
|
||||
|
||||
return [self componentsWithTextStorage:textStorage
|
||||
textContainerSize:textContainerSize
|
||||
layoutManager:[[NSLayoutManager alloc] init]];
|
||||
layoutManager:[[ASCustomLayoutManager alloc] init]];
|
||||
}
|
||||
|
||||
+ (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage
|
||||
|
@ -28,6 +28,7 @@ swift_library(
|
||||
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
|
||||
"//submodules/Pasteboard:Pasteboard",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -18,6 +18,7 @@ import InvisibleInkDustNode
|
||||
import TextInputMenu
|
||||
import ChatPresentationInterfaceState
|
||||
import Pasteboard
|
||||
import EmojiTextAttachmentView
|
||||
|
||||
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
|
||||
private let minInputFontSize: CGFloat = 5.0
|
||||
@ -208,7 +209,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
|
||||
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)
|
||||
self.updatingInputState = false
|
||||
self.updateTextNodeText(animated: animated)
|
||||
@ -241,6 +242,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
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) {
|
||||
self.context = context
|
||||
self.presentationInterfaceState = presentationInterfaceState
|
||||
@ -311,6 +314,14 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
}
|
||||
self.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
|
||||
|
||||
self.emojiViewProvider = { [weak self] emoji in
|
||||
guard let strongSelf = self, let file = strongSelf.context.animatedEmojiStickers[emoji]?.first?.file else {
|
||||
return UIView()
|
||||
}
|
||||
|
||||
return EmojiTextAttachmentView(context: context, file: file)
|
||||
}
|
||||
|
||||
self.updateSendButtonEnabled(isCaption || isAttachment, animated: false)
|
||||
}
|
||||
|
||||
@ -741,7 +752,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
@objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState {
|
||||
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)
|
||||
|
||||
self.updateSpoiler()
|
||||
@ -876,9 +887,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
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 {
|
||||
let containerView = textInputNode.textView.subviews[1]
|
||||
@ -968,7 +979,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
let textFont = Font.regular(baseFontSize)
|
||||
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")
|
||||
if range.location != NSNotFound {
|
||||
@ -1245,7 +1256,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
|
||||
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)
|
||||
self.textInputNode?.attributedText = string
|
||||
self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0)
|
||||
|
@ -228,7 +228,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe
|
||||
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 measureRawString = measureText.string
|
||||
if measureRawString.hasSuffix("\n") || measureRawString.isEmpty {
|
||||
@ -294,11 +294,11 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe
|
||||
if let currentText = strongSelf.textNode.attributedText {
|
||||
if currentText.string != attributedText.string || updatedTheme != nil {
|
||||
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 {
|
||||
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 {
|
||||
@ -514,7 +514,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe
|
||||
public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
if let item = self.item {
|
||||
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!)
|
||||
item.textUpdated(updatedText)
|
||||
} else {
|
||||
@ -544,7 +544,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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!)
|
||||
item.textUpdated(updatedText)
|
||||
|
@ -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
|
||||
|
||||
#if DEBUG
|
||||
#else
|
||||
let _ = (cachedFaqInstantPage(context: context)
|
||||
|> deliverOnMainQueue).start(next: { resolvedUrl 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)
|
||||
}, dismissInput: {}, contentContext: nil)
|
||||
})
|
||||
#endif
|
||||
})
|
||||
allItems.append(faq)
|
||||
|
||||
|
@ -18,11 +18,11 @@ public final class AnimatedStickerNodeLocalFileSource: AnimatedStickerNodeSource
|
||||
self.name = name
|
||||
}
|
||||
|
||||
public func directDataPath() -> Signal<String, NoError> {
|
||||
public func directDataPath(attemptSynchronously: Bool) -> Signal<String?, NoError> {
|
||||
if let path = self.path {
|
||||
return .single(path)
|
||||
} else {
|
||||
return .never()
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,10 @@ public final class AnimatedStickerNodeLocalFileSource: AnimatedStickerNodeSource
|
||||
return .never()
|
||||
}
|
||||
|
||||
func maybeCachedDataPath(width: Int, height: Int) -> (String, Bool)? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public var path: String? {
|
||||
if let path = getAppBundle().path(forResource: self.name, ofType: "tgs") {
|
||||
return path
|
||||
@ -64,13 +68,14 @@ public final class AnimatedStickerResourceSource: AnimatedStickerNodeSource {
|
||||
}
|
||||
}
|
||||
|
||||
public func directDataPath() -> Signal<String, NoError> {
|
||||
return self.account.postbox.mediaBox.resourceData(self.resource)
|
||||
|> filter { data in
|
||||
return data.complete
|
||||
}
|
||||
|> map { data -> String in
|
||||
public func directDataPath(attemptSynchronously: Bool) -> Signal<String?, NoError> {
|
||||
return self.account.postbox.mediaBox.resourceData(self.resource, attemptSynchronously: attemptSynchronously)
|
||||
|> map { data -> String? in
|
||||
if data.complete {
|
||||
return data.path
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ public struct UserLimitsConfiguration: Equatable {
|
||||
public let maxFolderChatsCount: Int32
|
||||
public let maxCaptionLengthCount: Int32
|
||||
public let maxUploadFileParts: Int32
|
||||
public let maxAnimatedEmojisInText: Int32
|
||||
|
||||
public static var defaultValue: UserLimitsConfiguration {
|
||||
return UserLimitsConfiguration(
|
||||
@ -22,7 +23,8 @@ public struct UserLimitsConfiguration: Equatable {
|
||||
maxFoldersCount: 10,
|
||||
maxFolderChatsCount: 100,
|
||||
maxCaptionLengthCount: 1024,
|
||||
maxUploadFileParts: 4000
|
||||
maxUploadFileParts: 4000,
|
||||
maxAnimatedEmojisInText: 10
|
||||
)
|
||||
}
|
||||
|
||||
@ -35,7 +37,8 @@ public struct UserLimitsConfiguration: Equatable {
|
||||
maxFoldersCount: Int32,
|
||||
maxFolderChatsCount: Int32,
|
||||
maxCaptionLengthCount: Int32,
|
||||
maxUploadFileParts: Int32
|
||||
maxUploadFileParts: Int32,
|
||||
maxAnimatedEmojisInText: Int32
|
||||
) {
|
||||
self.maxPinnedChatCount = maxPinnedChatCount
|
||||
self.maxChannelsCount = maxChannelsCount
|
||||
@ -46,6 +49,7 @@ public struct UserLimitsConfiguration: Equatable {
|
||||
self.maxFolderChatsCount = maxFolderChatsCount
|
||||
self.maxCaptionLengthCount = maxCaptionLengthCount
|
||||
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.maxChannelsCount = getValue("channels_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.maxCaptionLengthCount = getValue("caption_length_limit", orElse: defaultValue.maxCaptionLengthCount)
|
||||
self.maxUploadFileParts = getValue("upload_max_fileparts", orElse: defaultValue.maxUploadFileParts)
|
||||
self.maxAnimatedEmojisInText = getGeneralValue("message_animated_emoji_max", orElse: defaultValue.maxAnimatedEmojisInText)
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,30 @@
|
||||
import Postbox
|
||||
|
||||
public class AudioTranscriptionMessageAttribute: MessageAttribute, Equatable {
|
||||
public let locale: String
|
||||
public let id: Int64
|
||||
public let text: String
|
||||
|
||||
public var associatedPeerIds: [PeerId] {
|
||||
return []
|
||||
}
|
||||
|
||||
public init(locale: String, text: String) {
|
||||
self.locale = locale
|
||||
public init(id: Int64, text: String) {
|
||||
self.id = id
|
||||
self.text = text
|
||||
}
|
||||
|
||||
required public init(decoder: PostboxDecoder) {
|
||||
self.locale = decoder.decodeStringForKey("locale", orElse: "")
|
||||
self.id = decoder.decodeInt64ForKey("id", orElse: 0)
|
||||
self.text = decoder.decodeStringForKey("text", orElse: "")
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeString(self.locale, forKey: "locale")
|
||||
encoder.encodeInt64(self.id, forKey: "id")
|
||||
encoder.encodeString(self.text, forKey: "text")
|
||||
}
|
||||
|
||||
public static func ==(lhs: AudioTranscriptionMessageAttribute, rhs: AudioTranscriptionMessageAttribute) -> Bool {
|
||||
if lhs.locale != rhs.locale {
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
|
@ -57,6 +57,7 @@ public enum EngineConfiguration {
|
||||
public let maxFolderChatsCount: Int32
|
||||
public let maxCaptionLengthCount: Int32
|
||||
public let maxUploadFileParts: Int32
|
||||
public let maxAnimatedEmojisInText: Int32
|
||||
|
||||
public static var defaultValue: UserLimits {
|
||||
return UserLimits(UserLimitsConfiguration.defaultValue)
|
||||
@ -71,7 +72,8 @@ public enum EngineConfiguration {
|
||||
maxFoldersCount: Int32,
|
||||
maxFolderChatsCount: Int32,
|
||||
maxCaptionLengthCount: Int32,
|
||||
maxUploadFileParts: Int32
|
||||
maxUploadFileParts: Int32,
|
||||
maxAnimatedEmojisInText: Int32
|
||||
) {
|
||||
self.maxPinnedChatCount = maxPinnedChatCount
|
||||
self.maxChannelsCount = maxChannelsCount
|
||||
@ -82,6 +84,7 @@ public enum EngineConfiguration {
|
||||
self.maxFolderChatsCount = maxFolderChatsCount
|
||||
self.maxCaptionLengthCount = maxCaptionLengthCount
|
||||
self.maxUploadFileParts = maxUploadFileParts
|
||||
self.maxAnimatedEmojisInText = maxAnimatedEmojisInText
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -105,7 +108,7 @@ extension EngineConfiguration.Limits {
|
||||
}
|
||||
}
|
||||
|
||||
extension EngineConfiguration.UserLimits {
|
||||
public extension EngineConfiguration.UserLimits {
|
||||
init(_ userLimitsConfiguration: UserLimitsConfiguration) {
|
||||
self.init(
|
||||
maxPinnedChatCount: userLimitsConfiguration.maxPinnedChatCount,
|
||||
@ -116,7 +119,8 @@ extension EngineConfiguration.UserLimits {
|
||||
maxFoldersCount: userLimitsConfiguration.maxFoldersCount,
|
||||
maxFolderChatsCount: userLimitsConfiguration.maxFolderChatsCount,
|
||||
maxCaptionLengthCount: userLimitsConfiguration.maxCaptionLengthCount,
|
||||
maxUploadFileParts: userLimitsConfiguration.maxUploadFileParts
|
||||
maxUploadFileParts: userLimitsConfiguration.maxUploadFileParts,
|
||||
maxAnimatedEmojisInText: userLimitsConfiguration.maxAnimatedEmojisInText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -322,10 +322,14 @@ public extension TelegramEngine {
|
||||
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)
|
||||
}
|
||||
|
||||
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> {
|
||||
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)
|
||||
}
|
||||
|
@ -29,32 +29,70 @@ func _internal_translate(network: Network, text: String, fromLang: String?, toLa
|
||||
}
|
||||
}
|
||||
|
||||
public struct EngineAudioTranscriptionResult {
|
||||
public enum EngineAudioTranscriptionResult {
|
||||
public struct Success {
|
||||
public var id: Int64
|
||||
public var text: String
|
||||
|
||||
public init(id: Int64, text: String) {
|
||||
self.id = id
|
||||
self.text = text
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: MessageId) -> Signal<EngineAudioTranscriptionResult?, NoError> {
|
||||
case success(Success)
|
||||
case error
|
||||
}
|
||||
|
||||
func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: MessageId) -> Signal<EngineAudioTranscriptionResult, NoError> {
|
||||
return postbox.transaction { transaction -> Api.InputPeer? in
|
||||
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
|
||||
}
|
||||
|> mapToSignal { inputPeer -> Signal<EngineAudioTranscriptionResult?, NoError> in
|
||||
|> mapToSignal { inputPeer -> Signal<EngineAudioTranscriptionResult, NoError> in
|
||||
guard let inputPeer = inputPeer else {
|
||||
return .single(nil)
|
||||
return .single(.error)
|
||||
}
|
||||
return network.request(Api.functions.messages.transcribeAudio(peer: inputPeer, msgId: messageId.id))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.messages.TranscribedAudio?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<EngineAudioTranscriptionResult?, NoError> in
|
||||
|> mapToSignal { result -> Signal<EngineAudioTranscriptionResult, NoError> in
|
||||
guard let result = result else {
|
||||
return .single(nil)
|
||||
return .single(.error)
|
||||
}
|
||||
|
||||
return postbox.transaction { transaction -> EngineAudioTranscriptionResult in
|
||||
switch result {
|
||||
case let .transcribedAudio(transcriptionId, text):
|
||||
return .single(EngineAudioTranscriptionResult(id: transcriptionId, text: 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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -273,6 +273,8 @@ swift_library(
|
||||
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
|
||||
"//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent:AudioTranscriptionButtonComponent",
|
||||
"//submodules/TelegramUI/Components/AudioWaveformComponent:AudioWaveformComponent",
|
||||
"//submodules/TelegramUI/Components/EditableChatTextNode:EditableChatTextNode",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
"//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC",
|
||||
"//submodules/Media/LocalAudioTranscription:LocalAudioTranscription",
|
||||
] + select({
|
||||
|
@ -139,7 +139,7 @@ public final class AudioWaveformComponent: Component {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -317,7 +317,7 @@ public final class AudioWaveformComponent: Component {
|
||||
func update(component: AudioWaveformComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
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()
|
||||
}
|
||||
|
||||
@ -464,8 +464,6 @@ public final class AudioWaveformComponent: Component {
|
||||
}
|
||||
memset(adjustedSamplesMemory, 0, numSamples * 2)
|
||||
|
||||
var generateFakeSamples = false
|
||||
|
||||
var bins: [UInt16: Int] = [:]
|
||||
for i in 0 ..< maxReadSamples {
|
||||
let index = i * numSamples / maxReadSamples
|
||||
@ -491,27 +489,6 @@ public final class AudioWaveformComponent: Component {
|
||||
}
|
||||
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 commonRevealFraction = listViewAnimationCurveSystem(self.revealProgress)
|
||||
|
19
submodules/TelegramUI/Components/EditableChatTextNode/BUILD
Normal file
19
submodules/TelegramUI/Components/EditableChatTextNode/BUILD
Normal 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",
|
||||
],
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
|
||||
public final class EditableChatTextNode: EditableTextNode {
|
||||
}
|
@ -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",
|
||||
],
|
||||
)
|
@ -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))
|
||||
}
|
||||
}
|
@ -159,12 +159,20 @@ public final class AccountContextImpl: AccountContext {
|
||||
public let cachedGroupCallContexts: AccountGroupCallContextCache
|
||||
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)
|
||||
{
|
||||
self.sharedContextImpl = sharedContext
|
||||
self.account = account
|
||||
self.engine = TelegramEngine(account: account)
|
||||
|
||||
self.userLimits = EngineConfiguration.UserLimits(UserLimitsConfiguration.defaultValue)
|
||||
|
||||
self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager)
|
||||
|
||||
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
|
||||
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 {
|
||||
@ -252,6 +294,7 @@ public final class AccountContextImpl: AccountContext {
|
||||
self.contentSettingsDisposable?.dispose()
|
||||
self.appConfigurationDisposable?.dispose()
|
||||
self.experimentalUISettingsDisposable?.dispose()
|
||||
self.animatedEmojiStickersDisposable?.dispose()
|
||||
}
|
||||
|
||||
public func storeSecureIdPassword(password: String) {
|
||||
|
@ -138,6 +138,7 @@ public final class ChatControllerInteraction {
|
||||
|
||||
var canPlayMedia: Bool = false
|
||||
var hiddenMedia: [MessageId: [Media]] = [:]
|
||||
var expandedTranslationMessageStableIds: Set<UInt32> = Set()
|
||||
var selectionState: ChatInterfaceSelectionState?
|
||||
var highlightedState: ChatInterfaceHighlightedState?
|
||||
var contextHighlightedState: ChatInterfaceHighlightedState?
|
||||
|
@ -1932,9 +1932,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
case .none:
|
||||
break
|
||||
case .inputButtons:
|
||||
if let peer = self.chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil {
|
||||
} else {
|
||||
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
|
||||
return (.none, state.keyboardButtonsMessage?.id ?? state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
|
||||
})
|
||||
}
|
||||
default:
|
||||
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
|
||||
return (.none, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
|
||||
@ -2427,7 +2430,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
for text in breakChatInputText(trimChatInputText(inputText)) {
|
||||
if text.length != 0 {
|
||||
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 {
|
||||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||||
}
|
||||
|
@ -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 {
|
||||
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"
|
||||
@ -716,10 +735,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
})
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
actions.append(.separator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isReplyThreadHead = false
|
||||
if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation {
|
||||
@ -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
|
||||
if messageText.isEmpty {
|
||||
for media in message.media {
|
||||
@ -2293,3 +2322,162 @@ private func stringForRemainingTime(_ duration: Int32, strings: PresentationStri
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -175,7 +175,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
private var fileIconImage: UIImage?
|
||||
|
||||
private var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .possible
|
||||
private var transcribedText: String?
|
||||
private var transcribedText: EngineAudioTranscriptionResult?
|
||||
private var transcribeDisposable: Disposable?
|
||||
|
||||
override init() {
|
||||
@ -305,6 +305,17 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
guard let context = self.context, let message = self.message, let presentationData = self.presentationData else {
|
||||
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.transcribeDisposable == nil {
|
||||
self.audioTranscriptionState = .inProgress
|
||||
@ -352,7 +363,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
return
|
||||
}
|
||||
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 {
|
||||
strongSelf.audioTranscriptionState = .expanded
|
||||
} else {
|
||||
@ -368,7 +383,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
strongSelf.transcribeDisposable = nil
|
||||
strongSelf.audioTranscriptionState = .expanded
|
||||
strongSelf.transcribedText = result?.text
|
||||
strongSelf.transcribedText = result
|
||||
strongSelf.requestUpdateLayout(true)
|
||||
})
|
||||
}
|
||||
@ -573,7 +588,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
let textFont = arguments.presentationData.messageFont
|
||||
let textString: NSAttributedString?
|
||||
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 {
|
||||
textString = nil
|
||||
}
|
||||
|
@ -91,10 +91,11 @@ private final class InlineStickerItemLayer: SimpleLayer {
|
||||
|
||||
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)
|
||||
|> 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
|
||||
}
|
||||
Queue.mainQueue().async {
|
||||
@ -161,12 +162,6 @@ private final class InlineStickerItemLayer: SimpleLayer {
|
||||
guard let frameSource = self.frameSource else {
|
||||
return
|
||||
}
|
||||
if self.contents != nil {
|
||||
return
|
||||
}
|
||||
if self.didRequestFrame {
|
||||
return
|
||||
}
|
||||
self.didRequestFrame = true
|
||||
frameSource.with { [weak self] impl in
|
||||
if let animationFrame = impl.takeFrame(draw: true) {
|
||||
@ -200,9 +195,6 @@ private final class InlineStickerItemLayer: SimpleLayer {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.contents != nil {
|
||||
return
|
||||
}
|
||||
strongSelf.contents = image.cgImage
|
||||
}
|
||||
}
|
||||
@ -473,8 +465,38 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
attributedText = NSAttributedString(string: " ", font: textFont, textColor: messageTheme.primaryTextColor)
|
||||
}
|
||||
|
||||
/*if item.context.sharedContext.immediateExperimentalUISettings.inlineStickers*/ do {
|
||||
var currentCount = 0
|
||||
if let entities = entities {
|
||||
let updatedString = NSMutableAttributedString(attributedString: attributedText)
|
||||
|
||||
for entity in entities.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) {
|
||||
guard case .AnimatedEmoji = entity.type else {
|
||||
continue
|
||||
}
|
||||
|
||||
let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||
|
||||
let substring = updatedString.attributedSubstring(from: range)
|
||||
|
||||
let emoji = substring.string.basicEmoji.0
|
||||
|
||||
var emojiFile: TelegramMediaFile?
|
||||
emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
|
||||
if emojiFile == nil {
|
||||
emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -508,6 +530,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if !hadUpdates || currentCount >= 10 {
|
||||
break
|
||||
}
|
||||
}*/
|
||||
}
|
||||
attributedText = updatedString
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ import Pasteboard
|
||||
import ChatPresentationInterfaceState
|
||||
import ManagedAnimationNode
|
||||
import AttachmentUI
|
||||
import EditableChatTextNode
|
||||
import EmojiTextAttachmentView
|
||||
|
||||
private let accessoryButtonFont = Font.medium(14.0)
|
||||
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
|
||||
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)
|
||||
|
||||
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.keepSendButtonEnabled = keepSendButtonEnabled
|
||||
self.extendedSearchLayout = extendedSearchLayout
|
||||
@ -452,6 +459,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
|
||||
private var touchDownGestureRecognizer: TouchDownGestureRecognizer?
|
||||
|
||||
private var emojiViewProvider: ((String) -> UIView)?
|
||||
|
||||
init(presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
|
||||
self.presentationInterfaceState = presentationInterfaceState
|
||||
|
||||
@ -657,6 +666,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
self.textInputBackgroundNode.isUserInteractionEnabled = true
|
||||
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) {
|
||||
@ -674,7 +691,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
|
||||
private func loadTextInputNode() {
|
||||
let textInputNode = EditableTextNode()
|
||||
let textInputNode = EditableChatTextNode()
|
||||
textInputNode.initialPrimaryLanguage = self.presentationInterfaceState?.interfaceState.inputLanguage
|
||||
var textColor: UIColor = .black
|
||||
var tintColor: UIColor = .blue
|
||||
@ -1858,7 +1875,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState {
|
||||
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)
|
||||
|
||||
self.updateSpoiler()
|
||||
@ -1983,9 +2000,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
|
||||
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 {
|
||||
let containerView = textInputNode.textView.subviews[1]
|
||||
@ -2310,6 +2327,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage
|
||||
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? {
|
||||
@ -2487,7 +2510,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
|
||||
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)
|
||||
self.textInputNode?.attributedText = string
|
||||
self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0)
|
||||
|
@ -2929,7 +2929,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
|> `catch` { _ -> Signal<Bool, NoError> in
|
||||
return .single(false)
|
||||
}))
|
||||
|
||||
#if DEBUG
|
||||
#else
|
||||
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())
|
||||
|
||||
|
@ -14,6 +14,7 @@ swift_library(
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/Markdown:Markdown",
|
||||
"//submodules/Emoji:Emoji",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -4,6 +4,7 @@ import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import Emoji
|
||||
|
||||
private let alphanumericCharacters = CharacterSet.alphanumerics
|
||||
|
||||
@ -21,12 +22,28 @@ public struct ChatTextInputAttributes {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
text.enumerateAttributes(in: fullRange, options: [], using: { attributes, range, _ in
|
||||
sourceString.enumerateAttributes(in: fullRange, options: [], using: { attributes, range, _ in
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -47,7 +64,7 @@ public struct ChatTextFontAttributes: OptionSet {
|
||||
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 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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
return
|
||||
}
|
||||
@ -427,16 +474,18 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme
|
||||
var attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(initialAttributedText))
|
||||
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
|
||||
fullRange = NSRange(location: 0, length: initialAttributedText.length)
|
||||
fullRange = NSRange(location: 0, length: text.length)
|
||||
attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText))
|
||||
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) {
|
||||
fullRange = NSRange(location: 0, length: textNode.textView.textStorage.length)
|
||||
|
||||
textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange)
|
||||
textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.foregroundColor, 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 {
|
||||
return
|
||||
}
|
||||
@ -523,14 +618,14 @@ public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, th
|
||||
var text: NSString = initialAttributedText.string as NSString
|
||||
var fullRange = NSRange(location: 0, length: initialAttributedText.length)
|
||||
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
|
||||
fullRange = NSRange(location: 0, length: initialAttributedText.length)
|
||||
attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText))
|
||||
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) {
|
||||
textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange)
|
||||
@ -805,3 +900,39 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import TelegramCore
|
||||
import Emoji
|
||||
|
||||
private let whitelistedHosts: Set<String> = Set([
|
||||
"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] = []
|
||||
|
||||
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
|
||||
for (key, value) in attributes {
|
||||
if key == ChatTextInputAttributes.bold {
|
||||
|
Loading…
x
Reference in New Issue
Block a user