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 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?
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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({
|
||||||
|
@ -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)
|
||||||
|
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 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) {
|
||||||
|
@ -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?
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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())
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user