diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5e39ed1472..53975e99e0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -106,6 +106,7 @@ "PUSH_MESSAGE_FILES_TEXT_any" = "sent you %d files"; "PUSH_MESSAGE_THEME" = "%1$@|changed chat theme to %2$@"; "PUSH_MESSAGE_NOTHEME" = "%1$@|disabled chat theme"; +"PUSH_MESSAGE_RECURRING_PAY" = "%1$@|You were charged %2$@"; "PUSH_CHANNEL_MESSAGE_TEXT" = "%1$@|%2$@"; "PUSH_CHANNEL_MESSAGE_NOTEXT" = "%1$@|posted a message"; @@ -7542,7 +7543,7 @@ Sorry for the inconvenience."; "Channel.AddUserKickedError" = "Sorry, you can't add this user because they are on the list of Removed Users and you can't unban them."; "Channel.AddAdminKickedError" = "Sorry, you can't add this user as an admin because they are in the Removed Users list and you can't unban them."; -"Premium.Stickers.Description" = "Unlock this sticker and more by subscribing to Telegram Premium."; +"Premium.Stickers.Description" = "Unlock this sticker and many more by subscribing to Telegram Premium."; "Premium.Stickers.Proceed" = "Unlock Premium Stickers"; "Premium.Reactions.Proceed" = "Unlock Premium Reactions"; @@ -7591,9 +7592,9 @@ Sorry for the inconvenience."; "Premium.MaxFileSizeNoPremiumText" = "The document can't be sent, because it is larger than **%@**. We are working to let you increase this limit in the future."; "Premium.MaxFileSizeFinalText" = "The document can't be sent, because it is larger than **%@**."; -"Premium.MaxPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some of the currently pinned ones or subscribe to **Telegram Premium** to double the limit to **%2$@** chats."; -"Premium.MaxPinsNoPremiumText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some of the currently pinned ones."; -"Premium.MaxPinsFinalText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some of the currently pinned ones."; +"Premium.MaxPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some that are currently pinned or subscribe to **Telegram Premium** to double the limit to **%2$@** chats."; +"Premium.MaxPinsNoPremiumText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some that are currently pinned."; +"Premium.MaxPinsFinalText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some that are currently pinned."; "Premium.MaxFavedStickersTitle" = "The Limit of %@ Stickers Reached"; "Premium.MaxFavedStickersText" = "An older sticker was replaced with this one. You can [increase the limit]() to %@ stickers."; @@ -7603,7 +7604,7 @@ Sorry for the inconvenience."; "Premium.MaxSavedGifsText" = "An older GIF was replaced with this one. You can [increase the limit]() to %@ GIFS."; "Premium.MaxSavedGifsFinalText" = "An older GIF was replaced with this one."; -"Premium.MaxAccountsText" = "You have reached the limit of **%@** connected accounts. You can free one place by subscribing to **Telegram Premium**."; +"Premium.MaxAccountsText" = "You have reached the limit of **%@** connected accounts. You can add more by subscribing to **Telegram Premium**."; "Premium.MaxAccountsNoPremiumText" = "You have reached the limit of **%@** connected accounts."; "Premium.MaxAccountsFinalText" = "You have reached the limit of **%@** connected accounts."; @@ -7634,10 +7635,10 @@ Sorry for the inconvenience."; "Premium.NoAds" = "No Ads"; "Premium.NoAdsInfo" = "No more ads in public channels where Telegram sometimes shows ads."; -"Premium.NoAdsStandaloneInfo" = "Remove ads such as this by subscribing to **Telegram Premium**."; +"Premium.NoAdsStandaloneInfo" = "Remove ads such as this one by subscribing to **Telegram Premium**."; "Premium.Reactions" = "Unique Reactions"; -"Premium.ReactionsInfo" = "Additional animated reactions on messages, available only to the Premium subscribers."; +"Premium.ReactionsInfo" = "Additional animated reactions on messages, available only to Premium subscribers."; "Premium.ReactionsStandalone" = "Additional Reactions"; "Premium.ReactionsStandaloneInfo" = "Unlock a wider range of reactions on messages by subscribing to **Telegram Premium**."; @@ -7646,7 +7647,7 @@ Sorry for the inconvenience."; "Premium.StickersInfo" = "Exclusive enlarged stickers featuring additional effects, updated monthly."; "Premium.ChatManagement" = "Advanced Chat Management"; -"Premium.ChatManagementInfo" = "Tools to set default folder, auto-archive and hide new chats."; +"Premium.ChatManagementInfo" = "Tools to set the default folder, auto-archive and hide new chats from non-contacts."; "Premium.Badge" = "Profile Badge"; "Premium.BadgeInfo" = "A badge next to your name showing that you are helping support Telegram."; @@ -7665,10 +7666,10 @@ Sorry for the inconvenience."; "Premium.AboutTitle" = "ABOUT TELEGRAM PREMIUM"; "Premium.AboutText" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone."; -"Premium.Terms" = "By purchasing a Premium subscription, you agree to our [Terms of Service](terms) and [Privacy Policy](privacy)."; +"Premium.Terms" = "By purchasing a Premium subscription, you agree to the Telegram [Terms of Service](terms) and [Privacy Policy](privacy)."; "Conversation.CopyProtectionSavingDisabledSecret" = "Saving is restricted"; -"Conversation.CopyProtectionForwardingDisabledSecret" = "Forwards are restricted"; +"Conversation.CopyProtectionForwardingDisabledSecret" = "Forwarding is restricted"; "Settings.Terms_URL" = "https://telegram.org/tos"; "Settings.PrivacyPolicy_URL" = "https://telegram.org/privacy"; @@ -7676,7 +7677,7 @@ Sorry for the inconvenience."; "Stickers.PremiumPackInfoText" = "This pack contains premium stickers like this one."; "Stickers.PremiumPackView" = "View"; -"Conversation.PremiumUploadFileTooLarge" = "File could not be sent, because it is larger than 4 GB.\n\nYou can send as many files as you like, but each must be smaller than 4 GB."; +"Conversation.PremiumUploadFileTooLarge" = "File could not be sent because it is larger than 4 GB.\n\nYou can send as many files as you like, but each must be smaller than 4 GB."; "SponsoredMessageMenu.Hide" = "Hide"; @@ -7707,7 +7708,7 @@ Sorry for the inconvenience."; "WebApp.Settings" = "Settings"; -"Bot.AccepRecurrentInfo" = "I accept [Terms of Service]() of **%1$@**"; +"Bot.AccepRecurrentInfo" = "I accept the [Terms of Service]() of **%1$@**"; "Chat.AudioTranscriptionRateAction" = "Rate Transcription"; "Chat.AudioTranscriptionFeedbackTip" = "Thank you for your feedback."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index a8cc36c4a2..f19aa57c43 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -724,7 +724,7 @@ public protocol SharedAccountContext: AnyObject { func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set) -> Signal func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set, messages: [EngineMessage.Id: EngineMessage], peers: [EnginePeer.Id: EnginePeer]) -> Signal func resolveUrl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal - func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) + func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void) func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 63a69e990c..87464bb853 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -249,6 +249,7 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case monospace case textMention(EnginePeer.Id) case textUrl(String) + case customEmoji(stickerPack: StickerPackReference, fileId: Int64) public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) @@ -266,6 +267,10 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case 4: let url = (try? container.decode(String.self, forKey: "url")) ?? "" self = .textUrl(url) + case 5: + let stickerPack = try container.decode(StickerPackReference.self, forKey: "s") + let fileId = try container.decode(Int64.self, forKey: "f") + self = .customEmoji(stickerPack: stickerPack, fileId: fileId) default: assertionFailure() self = .bold @@ -287,6 +292,10 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case let .textUrl(url): try container.encode(4 as Int32, forKey: "t") try container.encode(url, forKey: "url") + case let .customEmoji(stickerPack, fileId): + try container.encode(5 as Int32, forKey: "t") + try container.encode(stickerPack, forKey: "s") + try container.encode(fileId, forKey: "f") } } } @@ -352,6 +361,8 @@ public struct ChatTextInputStateText: Codable, Equatable { parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textMention(value.peerId), range: range.location ..< (range.location + range.length))) } else if key == ChatTextInputAttributes.textUrl, let value = value as? ChatTextInputTextUrlAttribute { parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textUrl(value.url), range: range.location ..< (range.location + range.length))) + } else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { + parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: value.stickerPack, fileId: value.fileId), range: range.location ..< (range.location + range.length))) } } }) @@ -388,6 +399,8 @@ public struct ChatTextInputStateText: Codable, Equatable { result.addAttribute(ChatTextInputAttributes.textMention, value: ChatTextInputTextMentionAttribute(peerId: id), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) case let .textUrl(url): result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + case let .customEmoji(stickerPack, fileId): + result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) } } return result diff --git a/submodules/AccountContext/Sources/FetchManager.swift b/submodules/AccountContext/Sources/FetchManager.swift index 1646bfef07..a01a4b66b6 100644 --- a/submodules/AccountContext/Sources/FetchManager.swift +++ b/submodules/AccountContext/Sources/FetchManager.swift @@ -3,6 +3,7 @@ import Postbox import TelegramCore import SwiftSignalKit import TelegramUIPreferences +import RangeSet public enum FetchManagerCategory: Int32 { case image @@ -151,7 +152,7 @@ public enum FetchManagerPriority: Comparable { public protocol FetchManager { var queue: Queue { get } - func interactivelyFetched(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, mediaReference: AnyMediaReference?, resourceReference: MediaResourceReference, ranges: IndexSet, statsCategory: MediaResourceStatsCategory, elevatedPriority: Bool, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal + func interactivelyFetched(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, mediaReference: AnyMediaReference?, resourceReference: MediaResourceReference, ranges: RangeSet, statsCategory: MediaResourceStatsCategory, elevatedPriority: Bool, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal func cancelInteractiveFetches(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resource: MediaResource) func cancelInteractiveFetches(resourceId: String) func toggleInteractiveFetchPaused(resourceId: String, isPaused: Bool) diff --git a/submodules/AccountContext/Sources/FetchMediaUtils.swift b/submodules/AccountContext/Sources/FetchMediaUtils.swift index 64eea5d434..29bf1e3d89 100644 --- a/submodules/AccountContext/Sources/FetchMediaUtils.swift +++ b/submodules/AccountContext/Sources/FetchMediaUtils.swift @@ -4,6 +4,7 @@ import TelegramCore import Postbox import SwiftSignalKit import TelegramUIPreferences +import RangeSet public func freeMediaFileInteractiveFetched(account: Account, fileReference: FileMediaReference) -> Signal { return fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(fileReference.media.resource)) @@ -12,7 +13,7 @@ public func freeMediaFileInteractiveFetched(account: Account, fileReference: Fil public func freeMediaFileInteractiveFetched(fetchManager: FetchManager, fileReference: FileMediaReference, priority: FetchManagerPriority) -> Signal { let file = fileReference.media let mediaReference = AnyMediaReference.standalone(media: fileReference.media) - return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(PeerId(0)), locationKey: .free, mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: IndexSet(integersIn: 0 ..< Int(Int64.max) as Range), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil) + return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(PeerId(0)), locationKey: .free, mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: RangeSet(0 ..< Int64.max), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil) } public func freeMediaFileResourceInteractiveFetched(account: Account, fileReference: FileMediaReference, resource: MediaResource) -> Signal { @@ -37,7 +38,7 @@ public func messageMediaFileInteractiveFetched(context: AccountContext, message: return messageMediaFileInteractiveFetched(fetchManager: context.fetchManager, messageId: message.id, messageReference: MessageReference(message), file: file, userInitiated: userInitiated, priority: .userInitiated) } -public func messageMediaFileInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, file: TelegramMediaFile, ranges: IndexSet = IndexSet(integersIn: 0 ..< Int(Int64.max) as Range), userInitiated: Bool, priority: FetchManagerPriority) -> Signal { +public func messageMediaFileInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, file: TelegramMediaFile, ranges: RangeSet = RangeSet(0 ..< Int64.max), userInitiated: Bool, priority: FetchManagerPriority) -> Signal { let mediaReference = AnyMediaReference.message(message: messageReference, media: file) return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: ranges, statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerType: nil) } @@ -52,11 +53,11 @@ public func messageMediaImageInteractiveFetched(context: AccountContext, message public func messageMediaImageInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, image: TelegramMediaImage, resource: MediaResource, range: Range? = nil, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal { let mediaReference = AnyMediaReference.message(message: messageReference, media: image) - let ranges: IndexSet + let ranges: RangeSet if let range = range { - ranges = IndexSet(integersIn: Int(range.lowerBound) ..< Int(range.upperBound)) + ranges = RangeSet(range.lowerBound ..< range.upperBound) } else { - ranges = IndexSet(integersIn: Int(0) ..< Int(Int64.max)) + ranges = RangeSet(0 ..< Int64.max) } return fetchManager.interactivelyFetched(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(resource), ranges: ranges, statsCategory: .image, elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerType: storeToDownloadsPeerType) } diff --git a/submodules/AccountContext/Sources/OpenChatMessage.swift b/submodules/AccountContext/Sources/OpenChatMessage.swift index 57da66c17d..b5eb81cad0 100644 --- a/submodules/AccountContext/Sources/OpenChatMessage.swift +++ b/submodules/AccountContext/Sources/OpenChatMessage.swift @@ -36,7 +36,7 @@ public final class OpenChatMessageParams { public let openPeer: (Peer, ChatControllerInteractionNavigateToPeer) -> Void public let callPeer: (PeerId, Bool) -> Void public let enqueueMessage: (EnqueueMessage) -> Void - public let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + public let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? public let setupTemporaryHiddenMedia: (Signal, Int, Media) -> Void public let chatAvatarHiddenMedia: (Signal, Media) -> Void public let actionInteraction: GalleryControllerActionInteraction? @@ -63,7 +63,7 @@ public final class OpenChatMessageParams { openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void, callPeer: @escaping (PeerId, Bool) -> Void, enqueueMessage: @escaping (EnqueueMessage) -> Void, - sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, + sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, setupTemporaryHiddenMedia: @escaping (Signal, Int, Media) -> Void, chatAvatarHiddenMedia: @escaping (Signal, Media) -> Void, actionInteraction: GalleryControllerActionInteraction? = nil, diff --git a/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift b/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift index 166f7cdf80..2038bbed6c 100644 --- a/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift +++ b/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift @@ -272,6 +272,10 @@ private final class VideoStickerFrameSourceCache { private let useCache = true +public func makeVideoStickerDirectFrameSource(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?) -> AnimatedStickerFrameSource? { + return VideoStickerDirectFrameSource(queue: queue, path: path, width: width, height: height, cachePathPrefix: cachePathPrefix) +} + final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource { private let queue: Queue private let path: String diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm index c73adc2330..7bfee153fa 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm +++ b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm @@ -19,14 +19,14 @@ - (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(nullable CGRect *)remainingRect { CGRect result = [super lineFragmentRectForProposedRect:proposedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect]; -#if DEBUG +/*#if DEBUG if (result.origin.y < 10.0f) { result.size.width -= 21.0f; if (result.size.width < 0.0f) { result.size.width = 0.0f; } } -#endif +#endif*/ return result; } @@ -39,16 +39,17 @@ @implementation ASCustomLayoutManager -- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin { - /*CGGlyph glyph = [self glyphAtIndex:glyphsToShow.location]; - if (glyph) { +- (void)showCGGlyphs:(const CGGlyph *)glyphs positions:(const CGPoint *)positions count:(NSUInteger)glyphCount font:(UIFont *)font matrix:(CGAffineTransform)textMatrix attributes:(NSDictionary *)attributes inContext:(CGContextRef)graphicsContext { + for (NSUInteger i = 0; i < glyphCount; i++) { + if (attributes[@"Attribute__CustomEmoji"] != nil) { + continue; + } + + [super showCGGlyphs:&glyphs[i] positions:&positions[i] count:1 font:font matrix:textMatrix attributes:attributes inContext:graphicsContext]; } - - CGRect bounds = [self boundingRectForGlyphRange:glyphsToShow inTextContainer:[self textContainerForGlyphAtIndex:glyphsToShow.location effectiveRange:nil]]; - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextSetFillColorWithColor(context, [UIColor grayColor].CGColor); - CGContextFillRect(context, bounds);*/ - +} + +- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin { [super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin]; } diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index 43a96c5d10..c0afa5705a 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -242,7 +242,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS private var spoilersRevealed = false - private var emojiViewProvider: ((String) -> UIView)? + private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? private var maxCaptionLength: Int32? diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 80203f71c7..0cb84a5b20 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -593,7 +593,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { - }, sendSticker: { _, _, _, _ in + }, sendSticker: { _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in @@ -691,6 +691,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { }, displayCopyProtectionTip: { _, _ in }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in + }, insertText: { _ in + }, backwardsDeleteText: { }, chatController: { return nil }, statuses: nil) diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 5b2d46fcbb..89803b3707 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -98,7 +98,7 @@ public final class ChatPanelInterfaceInteraction { public let displayVideoUnmuteTip: (CGPoint?) -> Void public let switchMediaRecordingMode: () -> Void public let setupMessageAutoremoveTimeout: () -> Void - public let sendSticker: (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool + public let sendSticker: (FileMediaReference, Bool, UIView, CGRect, CALayer?) -> Bool public let unblockPeer: () -> Void public let pinMessage: (MessageId, ContextControllerProtocol?) -> Void public let unpinMessage: (MessageId, Bool, ContextControllerProtocol?) -> Void @@ -123,7 +123,7 @@ public final class ChatPanelInterfaceInteraction { public let unarchiveChat: () -> Void public let openLinkEditing: () -> Void public let reportPeerIrrelevantGeoLocation: () -> Void - public let displaySlowmodeTooltip: (ASDisplayNode, CGRect) -> Void + public let displaySlowmodeTooltip: (UIView, CGRect) -> Void public let displaySendMessageOptions: (ASDisplayNode, ContextGesture) -> Void public let openScheduledMessages: () -> Void public let displaySearchResultsTooltip: (ASDisplayNode, CGRect) -> Void @@ -144,6 +144,8 @@ public final class ChatPanelInterfaceInteraction { public let displayCopyProtectionTip: (ASDisplayNode, Bool) -> Void public let openWebView: (String, String, Bool, Bool) -> Void public let updateShowWebView: ((Bool) -> Bool) -> Void + public let insertText: (NSAttributedString) -> Void + public let backwardsDeleteText: () -> Void public let chatController: () -> ViewController? public let statuses: ChatPanelInterfaceInteractionStatuses? @@ -192,7 +194,7 @@ public final class ChatPanelInterfaceInteraction { displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, - sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, + sendSticker: @escaping (FileMediaReference, Bool, UIView, CGRect, CALayer?) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId, ContextControllerProtocol?) -> Void, unpinMessage: @escaping (MessageId, Bool, ContextControllerProtocol?) -> Void, @@ -217,7 +219,7 @@ public final class ChatPanelInterfaceInteraction { unarchiveChat: @escaping () -> Void, openLinkEditing: @escaping () -> Void, reportPeerIrrelevantGeoLocation: @escaping () -> Void, - displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, + displaySlowmodeTooltip: @escaping (UIView, CGRect) -> Void, displaySendMessageOptions: @escaping (ASDisplayNode, ContextGesture) -> Void, openScheduledMessages: @escaping () -> Void, openPeersNearby: @escaping () -> Void, @@ -238,6 +240,8 @@ public final class ChatPanelInterfaceInteraction { displayCopyProtectionTip: @escaping (ASDisplayNode, Bool) -> Void, openWebView: @escaping (String, String, Bool, Bool) -> Void, updateShowWebView: @escaping ((Bool) -> Bool) -> Void, + insertText: @escaping (NSAttributedString) -> Void, + backwardsDeleteText: @escaping () -> Void, chatController: @escaping () -> ViewController?, statuses: ChatPanelInterfaceInteractionStatuses? ) { @@ -331,6 +335,9 @@ public final class ChatPanelInterfaceInteraction { self.displayCopyProtectionTip = displayCopyProtectionTip self.openWebView = openWebView self.updateShowWebView = updateShowWebView + self.insertText = insertText + self.backwardsDeleteText = backwardsDeleteText + self.chatController = chatController self.statuses = statuses } @@ -384,7 +391,7 @@ public final class ChatPanelInterfaceInteraction { }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { - }, sendSticker: { _, _, _, _ in + }, sendSticker: { _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in @@ -431,6 +438,8 @@ public final class ChatPanelInterfaceInteraction { }, displayCopyProtectionTip: { _, _ in }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in + }, insertText: { _ in + }, backwardsDeleteText: { }, chatController: { return nil }, statuses: nil) diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index dc8a1a4308..1477eb4fbd 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -1,9 +1,17 @@ import Foundation import UIKit +#if targetEnvironment(simulator) +@_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float +#endif + private extension UIView { static var animationDurationFactor: Double { + #if targetEnvironment(simulator) + return Double(UIAnimationDragCoefficient()) + #else return 1.0 + #endif } } @@ -182,10 +190,12 @@ public struct Transition { switch self.animation { case .none: view.frame = frame + view.layer.removeAnimation(forKey: "position") + view.layer.removeAnimation(forKey: "bounds") completion?(true) case .curve: - let previousPosition = view.center - let previousBounds = view.bounds + let previousPosition = view.layer.presentation()?.position ?? view.center + let previousBounds = view.layer.presentation()?.bounds ?? view.bounds view.frame = frame self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion) @@ -201,9 +211,10 @@ public struct Transition { switch self.animation { case .none: view.bounds = bounds + view.layer.removeAnimation(forKey: "bounds") completion?(true) case .curve: - let previousBounds = view.bounds + let previousBounds = view.layer.presentation()?.bounds ?? view.bounds view.bounds = bounds self.animateBounds(view: view, from: previousBounds, to: view.bounds, completion: completion) @@ -218,9 +229,10 @@ public struct Transition { switch self.animation { case .none: view.center = position + view.layer.removeAnimation(forKey: "position") completion?(true) case .curve: - let previousPosition = view.center + let previousPosition = view.layer.presentation()?.position ?? view.center view.center = position self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion) @@ -235,9 +247,10 @@ public struct Transition { switch self.animation { case .none: view.alpha = alpha + view.layer.removeAnimation(forKey: "opacity") completion?(true) case .curve: - let previousAlpha = view.alpha + let previousAlpha = (view.layer.presentation()?.opacity).flatMap(CGFloat.init) ?? view.alpha view.alpha = alpha self.animateAlpha(view: view, from: previousAlpha, to: alpha, completion: completion) } diff --git a/submodules/Components/BlurredBackgroundComponent/BUILD b/submodules/Components/BlurredBackgroundComponent/BUILD new file mode 100644 index 0000000000..5474f19bf9 --- /dev/null +++ b/submodules/Components/BlurredBackgroundComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BlurredBackgroundComponent", + module_name = "BlurredBackgroundComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/BlurredBackgroundComponent/Sources/BlurredBackgroundComponent.swift b/submodules/Components/BlurredBackgroundComponent/Sources/BlurredBackgroundComponent.swift new file mode 100644 index 0000000000..0717467711 --- /dev/null +++ b/submodules/Components/BlurredBackgroundComponent/Sources/BlurredBackgroundComponent.swift @@ -0,0 +1,39 @@ +import Foundation +import UIKit +import ComponentFlow +import Display +import ComponentDisplayAdapters + +public final class BlurredBackgroundComponent: Component { + public let color: UIColor + + public init( + color: UIColor + ) { + self.color = color + } + + public static func ==(lhs: BlurredBackgroundComponent, rhs: BlurredBackgroundComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + public final class View: BlurredBackgroundView { + public func update(component: BlurredBackgroundComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.updateColor(color: component.color, transition: transition.containedViewLayoutTransition) + self.update(size: availableSize, transition: transition.containedViewLayoutTransition) + + return availableSize + } + } + + public func makeView() -> View { + return View(color: .clear, enableBlur: true) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift index fb3955c404..e055375462 100644 --- a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift +++ b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift @@ -7,10 +7,12 @@ import Display public final class BundleIconComponent: Component { public let name: String public let tintColor: UIColor? + public let maxSize: CGSize? - public init(name: String, tintColor: UIColor?) { + public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil) { self.name = name self.tintColor = tintColor + self.maxSize = maxSize } public static func ==(lhs: BundleIconComponent, rhs: BundleIconComponent) -> Bool { @@ -20,6 +22,9 @@ public final class BundleIconComponent: Component { if lhs.tintColor != rhs.tintColor { return false } + if lhs.maxSize != rhs.maxSize { + return false + } return true } @@ -44,7 +49,10 @@ public final class BundleIconComponent: Component { } self.component = component - let imageSize = self.image?.size ?? CGSize() + var imageSize = self.image?.size ?? CGSize() + if let maxSize = component.maxSize { + imageSize = imageSize.aspectFitted(maxSize) + } return CGSize(width: min(imageSize.width, availableSize.width), height: min(imageSize.height, availableSize.height)) } diff --git a/submodules/Components/ComponentDisplayAdapters/BUILD b/submodules/Components/ComponentDisplayAdapters/BUILD new file mode 100644 index 0000000000..50101fc7ad --- /dev/null +++ b/submodules/Components/ComponentDisplayAdapters/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ComponentDisplayAdapters", + module_name = "ComponentDisplayAdapters", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift b/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift new file mode 100644 index 0000000000..34fa9ce017 --- /dev/null +++ b/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift @@ -0,0 +1,50 @@ +import Foundation +import UIKit +import ComponentFlow +import Display + +public extension Transition.Animation.Curve { + init(_ curve: ContainedViewLayoutTransitionCurve) { + switch curve { + case .linear: + self = .easeInOut + case .easeInOut: + self = .easeInOut + case .custom: + self = .spring + case .customSpring: + self = .spring + case .spring: + self = .spring + } + } + + var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve { + switch self { + case .easeInOut: + return .easeInOut + case .spring: + return .spring + } + } +} + +public extension Transition { + init(_ transition: ContainedViewLayoutTransition) { + switch transition { + case .immediate: + self.init(animation: .none) + case let .animated(duration, curve): + self.init(animation: .curve(duration: duration, curve: Transition.Animation.Curve(curve))) + } + } + + var containedViewLayoutTransition: ContainedViewLayoutTransition { + switch self.animation { + case .none: + return .immediate + case let .curve(duration, curve): + return .animated(duration: duration, curve: curve.containedViewLayoutTransitionCurve) + } + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Components/EmojiKeyboard/BUILD b/submodules/Components/PagerComponent/BUILD similarity index 72% rename from submodules/TelegramUI/Components/EmojiKeyboard/BUILD rename to submodules/Components/PagerComponent/BUILD index 525cbd2996..ed1b87f808 100644 --- a/submodules/TelegramUI/Components/EmojiKeyboard/BUILD +++ b/submodules/Components/PagerComponent/BUILD @@ -1,8 +1,8 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") swift_library( - name = "EmojiKeyboard", - module_name = "EmojiKeyboard", + name = "PagerComponent", + module_name = "PagerComponent", srcs = glob([ "Sources/**/*.swift", ]), @@ -11,6 +11,7 @@ swift_library( ], deps = [ "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift new file mode 100644 index 0000000000..12b311e547 --- /dev/null +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -0,0 +1,579 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +public final class PagerComponentChildEnvironment: Equatable { + public struct ContentScrollingUpdate { + public var relativeOffset: CGFloat + public var absoluteOffsetToClosestEdge: CGFloat? + public var transition: Transition + + public init( + relativeOffset: CGFloat, + absoluteOffsetToClosestEdge: CGFloat?, + transition: Transition + ) { + self.relativeOffset = relativeOffset + self.absoluteOffsetToClosestEdge = absoluteOffsetToClosestEdge + self.transition = transition + } + } + + public let containerInsets: UIEdgeInsets + public let onChildScrollingUpdate: (ContentScrollingUpdate) -> Void + + init( + containerInsets: UIEdgeInsets, + onChildScrollingUpdate: @escaping (ContentScrollingUpdate) -> Void + ) { + self.containerInsets = containerInsets + self.onChildScrollingUpdate = onChildScrollingUpdate + } + + public static func ==(lhs: PagerComponentChildEnvironment, rhs: PagerComponentChildEnvironment) -> Bool { + if lhs.containerInsets != rhs.containerInsets { + return false + } + + return true + } +} + +public final class PagerComponentPanelEnvironment: Equatable { + public let contentOffset: CGFloat + public let contentTopPanels: [AnyComponentWithIdentity] + public let contentIcons: [AnyComponentWithIdentity] + public let contentAccessoryRightButtons: [AnyComponentWithIdentity] + public let activeContentId: AnyHashable? + public let navigateToContentId: (AnyHashable) -> Void + + init( + contentOffset: CGFloat, + contentTopPanels: [AnyComponentWithIdentity], + contentIcons: [AnyComponentWithIdentity], + contentAccessoryRightButtons: [AnyComponentWithIdentity], + activeContentId: AnyHashable?, + navigateToContentId: @escaping (AnyHashable) -> Void + ) { + self.contentOffset = contentOffset + self.contentTopPanels = contentTopPanels + self.contentIcons = contentIcons + self.contentAccessoryRightButtons = contentAccessoryRightButtons + self.activeContentId = activeContentId + self.navigateToContentId = navigateToContentId + } + + public static func ==(lhs: PagerComponentPanelEnvironment, rhs: PagerComponentPanelEnvironment) -> Bool { + if lhs.contentOffset != rhs.contentOffset { + return false + } + if lhs.contentTopPanels != rhs.contentTopPanels { + return false + } + if lhs.contentIcons != rhs.contentIcons { + return false + } + if lhs.contentAccessoryRightButtons != rhs.contentAccessoryRightButtons { + return false + } + if lhs.activeContentId != rhs.activeContentId { + return false + } + + return true + } +} + +public struct PagerComponentPanelState { + public var topPanelHeight: CGFloat + + public init(topPanelHeight: CGFloat) { + self.topPanelHeight = topPanelHeight + } +} + +public final class PagerComponent: Component { + public typealias EnvironmentType = ChildEnvironmentType + + public let contentInsets: UIEdgeInsets + public let contents: [AnyComponentWithIdentity<(ChildEnvironmentType, PagerComponentChildEnvironment)>] + public let contentTopPanels: [AnyComponentWithIdentity] + public let contentIcons: [AnyComponentWithIdentity] + public let contentAccessoryRightButtons:[AnyComponentWithIdentity] + public let defaultId: AnyHashable? + public let contentBackground: AnyComponent? + public let topPanel: AnyComponent? + public let externalTopPanelContainer: UIView? + public let bottomPanel: AnyComponent? + public let panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)? + + public init( + contentInsets: UIEdgeInsets, + contents: [AnyComponentWithIdentity<(ChildEnvironmentType, PagerComponentChildEnvironment)>], + contentTopPanels: [AnyComponentWithIdentity], + contentIcons: [AnyComponentWithIdentity], + contentAccessoryRightButtons:[AnyComponentWithIdentity], + defaultId: AnyHashable?, + contentBackground: AnyComponent?, + topPanel: AnyComponent?, + externalTopPanelContainer: UIView?, + bottomPanel: AnyComponent?, + panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)? + ) { + self.contentInsets = contentInsets + self.contents = contents + self.contentTopPanels = contentTopPanels + self.contentIcons = contentIcons + self.contentAccessoryRightButtons = contentAccessoryRightButtons + self.defaultId = defaultId + self.contentBackground = contentBackground + self.topPanel = topPanel + self.externalTopPanelContainer = externalTopPanelContainer + self.bottomPanel = bottomPanel + self.panelStateUpdated = panelStateUpdated + } + + public static func ==(lhs: PagerComponent, rhs: PagerComponent) -> Bool { + if lhs.contentInsets != rhs.contentInsets { + return false + } + if lhs.contents != rhs.contents { + return false + } + if lhs.contentTopPanels != rhs.contentTopPanels { + return false + } + if lhs.contentIcons != rhs.contentIcons { + return false + } + if lhs.defaultId != rhs.defaultId { + return false + } + if lhs.contentBackground != rhs.contentBackground { + return false + } + if lhs.topPanel != rhs.topPanel { + return false + } + if lhs.externalTopPanelContainer !== rhs.externalTopPanelContainer { + return false + } + if lhs.bottomPanel != rhs.bottomPanel { + return false + } + + return true + } + + public final class View: UIView { + private final class ContentView { + let view: ComponentHostView<(ChildEnvironmentType, PagerComponentChildEnvironment)> + var scrollingPanelOffsetToClosestEdge: CGFloat = 0.0 + + init(view: ComponentHostView<(ChildEnvironmentType, PagerComponentChildEnvironment)>) { + self.view = view + } + } + + private struct PaneTransitionGestureState { + var fraction: CGFloat = 0.0 + } + + private var contentViews: [AnyHashable: ContentView] = [:] + private var contentBackgroundView: ComponentHostView? + private var topPanelView: ComponentHostView? + private var bottomPanelView: ComponentHostView? + + private var centralId: AnyHashable? + private var paneTransitionGestureState: PaneTransitionGestureState? + + private var component: PagerComponent? + private weak var state: EmptyComponentState? + + private var panRecognizer: UIPanGestureRecognizer? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.disablesInteractiveTransitionGestureRecognizer = true + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.panRecognizer = panRecognizer + self.addGestureRecognizer(panRecognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.paneTransitionGestureState = PaneTransitionGestureState() + case .changed: + if var paneTransitionGestureState = self.paneTransitionGestureState, self.bounds.width > 0.0 { + paneTransitionGestureState.fraction = recognizer.translation(in: self).x / self.bounds.width + + self.paneTransitionGestureState = paneTransitionGestureState + self.state?.updated(transition: .immediate) + } + case .ended, .cancelled: + if let paneTransitionGestureState = self.paneTransitionGestureState { + self.paneTransitionGestureState = nil + + if paneTransitionGestureState.fraction != 0.0, let component = self.component, let centralId = self.centralId, let centralIndex = component.contents.firstIndex(where: { $0.id == centralId }) { + let fraction = recognizer.translation(in: self).x / self.bounds.width + let velocity = recognizer.velocity(in: self) + + var updatedCentralIndex = centralIndex + if abs(velocity.x) > 180.0 { + if velocity.x > 0.0 { + updatedCentralIndex = max(0, updatedCentralIndex - 1) + } else { + updatedCentralIndex = min(component.contents.count - 1, updatedCentralIndex + 1) + } + } else if abs(fraction) > 0.35 { + if fraction > 0.0 { + updatedCentralIndex = max(0, updatedCentralIndex - 1) + } else { + updatedCentralIndex = min(component.contents.count - 1, updatedCentralIndex + 1) + } + } + if updatedCentralIndex != centralIndex { + self.centralId = component.contents[updatedCentralIndex].id + } + } + + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + default: + break + } + } + + func update(component: PagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let navigateToContentId: (AnyHashable) -> Void = { [weak self] id in + guard let strongSelf = self else { + return + } + if strongSelf.centralId != id { + strongSelf.centralId = id + strongSelf.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + } + + var centralId: AnyHashable? + if let current = self.centralId { + if component.contents.contains(where: { $0.id == current }) { + centralId = current + } + } + if centralId == nil { + if let defaultId = component.defaultId { + if component.contents.contains(where: { $0.id == defaultId }) { + centralId = defaultId + } + } else { + centralId = component.contents.first?.id + } + } + + if self.centralId != centralId { + self.centralId = centralId + } + + var contentInsets = component.contentInsets + + let scrollingPanelOffsetToClosestEdge: CGFloat + if let centralId = centralId, let centralContentView = self.contentViews[centralId] { + scrollingPanelOffsetToClosestEdge = centralContentView.scrollingPanelOffsetToClosestEdge + } else { + scrollingPanelOffsetToClosestEdge = 0.0 + } + + var topPanelHeight: CGFloat = 0.0 + if let topPanel = component.topPanel { + let topPanelView: ComponentHostView + var topPanelTransition = transition + if let current = self.topPanelView { + topPanelView = current + } else { + topPanelTransition = .immediate + topPanelView = ComponentHostView() + topPanelView.clipsToBounds = true + self.topPanelView = topPanelView + } + + let topPanelSuperview = component.externalTopPanelContainer ?? self + if topPanelView.superview !== topPanelSuperview { + topPanelSuperview.addSubview(topPanelView) + } + + let topPanelSize = topPanelView.update( + transition: topPanelTransition, + component: topPanel, + environment: { + PagerComponentPanelEnvironment( + contentOffset: 0.0, + contentTopPanels: component.contentTopPanels, + contentIcons: [], + contentAccessoryRightButtons: [], + activeContentId: centralId, + navigateToContentId: navigateToContentId + ) + }, + containerSize: availableSize + ) + + let topPanelOffset = max(0.0, min(topPanelSize.height, scrollingPanelOffsetToClosestEdge)) + + topPanelHeight = max(0.0, topPanelSize.height - topPanelOffset) + + if component.externalTopPanelContainer != nil { + let visibleTopPanelHeight = max(0.0, topPanelSize.height - topPanelOffset) + transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(), size: CGSize(width: topPanelSize.width, height: visibleTopPanelHeight))) + } else { + transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelOffset), size: topPanelSize)) + } + + contentInsets.top += topPanelSize.height + } else { + if let bottomPanelView = self.bottomPanelView { + self.bottomPanelView = nil + + bottomPanelView.removeFromSuperview() + } + } + + var bottomPanelOffset: CGFloat = 0.0 + if let bottomPanel = component.bottomPanel { + let bottomPanelView: ComponentHostView + var bottomPanelTransition = transition + if let current = self.bottomPanelView { + bottomPanelView = current + } else { + bottomPanelTransition = .immediate + bottomPanelView = ComponentHostView() + self.bottomPanelView = bottomPanelView + self.addSubview(bottomPanelView) + } + let bottomPanelSize = bottomPanelView.update( + transition: bottomPanelTransition, + component: bottomPanel, + environment: { + PagerComponentPanelEnvironment( + contentOffset: 0.0, + contentTopPanels: [], + contentIcons: component.contentIcons, + contentAccessoryRightButtons: component.contentAccessoryRightButtons, + activeContentId: centralId, + navigateToContentId: navigateToContentId + ) + }, + containerSize: availableSize + ) + + bottomPanelOffset = max(0.0, min(bottomPanelSize.height, scrollingPanelOffsetToClosestEdge)) + + transition.setFrame(view: bottomPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height + bottomPanelOffset), size: bottomPanelSize)) + + contentInsets.bottom += bottomPanelSize.height + } else { + if let bottomPanelView = self.bottomPanelView { + self.bottomPanelView = nil + + bottomPanelView.removeFromSuperview() + } + } + + if let contentBackground = component.contentBackground { + let contentBackgroundView: ComponentHostView + var contentBackgroundTransition = transition + if let current = self.contentBackgroundView { + contentBackgroundView = current + } else { + contentBackgroundTransition = .immediate + contentBackgroundView = ComponentHostView() + self.contentBackgroundView = contentBackgroundView + self.insertSubview(contentBackgroundView, at: 0) + } + let contentBackgroundSize = contentBackgroundView.update( + transition: contentBackgroundTransition, + component: contentBackground, + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight - contentInsets.bottom + bottomPanelOffset) + ) + contentBackgroundTransition.setFrame(view: contentBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: contentBackgroundSize)) + } else { + if let contentBackgroundView = self.contentBackgroundView { + self.contentBackgroundView = nil + contentBackgroundView.removeFromSuperview() + } + } + + var validIds: [AnyHashable] = [] + if let centralId = self.centralId, let centralIndex = component.contents.firstIndex(where: { $0.id == centralId }) { + let contentSize = CGSize(width: availableSize.width, height: availableSize.height) + + var referenceFrames: [AnyHashable: CGRect] = [:] + if case .none = transition.animation { + } else { + for (id, contentView) in self.contentViews { + referenceFrames[id] = contentView.view.frame + } + } + + for index in 0 ..< component.contents.count { + let indexOffset = index - centralIndex + let clippedIndexOffset = max(-1, min(1, indexOffset)) + var checkingContentFrame = CGRect(origin: CGPoint(x: contentSize.width * CGFloat(indexOffset), y: 0.0), size: contentSize) + var contentFrame = CGRect(origin: CGPoint(x: contentSize.width * CGFloat(clippedIndexOffset), y: 0.0), size: contentSize) + + if let paneTransitionGestureState = self.paneTransitionGestureState { + checkingContentFrame.origin.x += paneTransitionGestureState.fraction * availableSize.width + contentFrame.origin.x += paneTransitionGestureState.fraction * availableSize.width + } + let content = component.contents[index] + + let isInBounds = CGRect(origin: CGPoint(), size: availableSize).intersects(checkingContentFrame) + + var isPartOfTransition = false + if case .none = transition.animation { + } else if self.contentViews[content.id] != nil { + isPartOfTransition = true + } + + if isInBounds || isPartOfTransition || content.id == centralId { + let id = content.id + validIds.append(content.id) + + var wasAdded = false + var contentTransition = transition + let contentView: ContentView + if let current = self.contentViews[content.id] { + contentView = current + } else { + wasAdded = true + contentView = ContentView(view: ComponentHostView<(ChildEnvironmentType, PagerComponentChildEnvironment)>()) + contentTransition = .immediate + self.contentViews[content.id] = contentView + if let contentBackgroundView = self.contentBackgroundView { + self.insertSubview(contentView.view, aboveSubview: contentBackgroundView) + } else { + self.insertSubview(contentView.view, at: 0) + } + } + + let pagerChildEnvironment = PagerComponentChildEnvironment( + containerInsets: contentInsets, + onChildScrollingUpdate: { [weak self] update in + guard let strongSelf = self else { + return + } + strongSelf.onChildScrollingUpdate(id: id, update: update) + } + ) + + let _ = contentView.view.update( + transition: contentTransition, + component: content.component, + environment: { + environment[ChildEnvironmentType.self] + pagerChildEnvironment + }, + containerSize: contentFrame.size + ) + + if wasAdded { + if case .none = transition.animation { + contentView.view.frame = contentFrame + } else { + var referenceDirectionIsRight: Bool? + for (previousId, previousFrame) in referenceFrames { + if let previousIndex = component.contents.firstIndex(where: { $0.id == previousId }) { + if previousFrame.minX == 0.0 { + if previousIndex < index { + referenceDirectionIsRight = true + } else { + referenceDirectionIsRight = false + } + break + } + } + } + if let referenceDirectionIsRight = referenceDirectionIsRight { + contentView.view.frame = contentFrame.offsetBy(dx: referenceDirectionIsRight ? contentFrame.width : (-contentFrame.width), dy: 0.0) + transition.setFrame(view: contentView.view, frame: contentFrame, completion: { [weak self] completed in + if completed && !isInBounds && isPartOfTransition { + DispatchQueue.main.async { + self?.state?.updated(transition: .immediate) + } + } + }) + } else { + contentView.view.frame = contentFrame + } + } + } else { + transition.setFrame(view: contentView.view, frame: contentFrame, completion: { [weak self] completed in + if completed && !isInBounds && isPartOfTransition { + DispatchQueue.main.async { + self?.state?.updated(transition: .immediate) + } + } + }) + } + } + } + } + + var removedIds: [AnyHashable] = [] + for (id, contentView) in self.contentViews { + if !validIds.contains(id) { + removedIds.append(id) + contentView.view.removeFromSuperview() + } + } + for id in removedIds { + self.contentViews.removeValue(forKey: id) + } + + if let panelStateUpdated = component.panelStateUpdated { + panelStateUpdated( + PagerComponentPanelState( + topPanelHeight: topPanelHeight + ), + transition + ) + } + + return availableSize + } + + private func onChildScrollingUpdate(id: AnyHashable, update: PagerComponentChildEnvironment.ContentScrollingUpdate) { + guard let contentView = self.contentViews[id] else { + return + } + + if let absoluteOffsetToClosestEdge = update.absoluteOffsetToClosestEdge { + contentView.scrollingPanelOffsetToClosestEdge = absoluteOffsetToClosestEdge + } else { + contentView.scrollingPanelOffsetToClosestEdge = 1000.0 + } + + state?.updated(transition: update.transition) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/Components/ViewControllerComponent/BUILD b/submodules/Components/ViewControllerComponent/BUILD index 0e1e28f483..ca5e4d38f0 100644 --- a/submodules/Components/ViewControllerComponent/BUILD +++ b/submodules/Components/ViewControllerComponent/BUILD @@ -15,6 +15,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index fcf07de3f0..a0677bdd9e 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -5,52 +5,7 @@ import Display import SwiftSignalKit import TelegramPresentationData import AccountContext - -public extension Transition.Animation.Curve { - init(_ curve: ContainedViewLayoutTransitionCurve) { - switch curve { - case .linear: - self = .easeInOut - case .easeInOut: - self = .easeInOut - case .custom: - self = .spring - case .customSpring: - self = .spring - case .spring: - self = .spring - } - } - - var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve { - switch self { - case .easeInOut: - return .easeInOut - case .spring: - return .spring - } - } -} - -public extension Transition { - init(_ transition: ContainedViewLayoutTransition) { - switch transition { - case .immediate: - self.init(animation: .none) - case let .animated(duration, curve): - self.init(animation: .curve(duration: duration, curve: Transition.Animation.Curve(curve))) - } - } - - var containedViewLayoutTransition: ContainedViewLayoutTransition { - switch self.animation { - case .none: - return .immediate - case let .curve(duration, curve): - return .animated(duration: duration, curve: curve.containedViewLayoutTransitionCurve) - } - } -} +import ComponentDisplayAdapters open class ViewControllerComponentContainer: ViewController { public enum NavigationBarAppearance { diff --git a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift index 975d030fef..0b71087ed0 100644 --- a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift +++ b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift @@ -199,7 +199,7 @@ public final class DirectMediaImageCache { ).start() let dataSignal: Signal - if resourceSizeLimit < Int(Int64.max) { + if resourceSizeLimit < Int64.max { dataSignal = self.account.postbox.mediaBox.resourceData(resource.resource, size: resourceSizeLimit, in: 0 ..< resourceSizeLimit) |> map { data, _ -> Data? in return data diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index 3a3c2f00fb..4d01d7ac2c 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -16,6 +16,10 @@ public enum DeviceMetrics: CaseIterable, Equatable { case iPhone12Mini case iPhone12 case iPhone12ProMax + case iPhone13Mini + case iPhone13 + case iPhone13Pro + case iPhone13ProMax case iPad case iPadMini case iPad102Inch @@ -38,6 +42,10 @@ public enum DeviceMetrics: CaseIterable, Equatable { .iPhone12Mini, .iPhone12, .iPhone12ProMax, + .iPhone13Mini, + .iPhone13, + .iPhone13Pro, + .iPhone13ProMax, .iPad, .iPadMini, .iPad102Inch, @@ -113,6 +121,14 @@ public enum DeviceMetrics: CaseIterable, Equatable { return CGSize(width: 390.0, height: 844.0) case .iPhone12ProMax: return CGSize(width: 428.0, height: 926.0) + case .iPhone13Mini: + return CGSize(width: 375.0, height: 812.0) + case .iPhone13: + return CGSize(width: 390.0, height: 844.0) + case .iPhone13Pro: + return CGSize(width: 390.0, height: 844.0) + case .iPhone13ProMax: + return CGSize(width: 428.0, height: 926.0) case .iPad: return CGSize(width: 768.0, height: 1024.0) case .iPadMini: @@ -140,9 +156,9 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 41.0 + UIScreenPixel case .iPhone12Mini: return 44.0 - case .iPhone12: + case .iPhone12, .iPhone13, .iPhone13Pro: return 47.0 + UIScreenPixel - case .iPhone12ProMax: + case .iPhone12ProMax, .iPhone13ProMax: return 53.0 + UIScreenPixel case let .unknown(_, _, onScreenNavigationHeight): if let _ = onScreenNavigationHeight { @@ -157,7 +173,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func safeInsets(inLandscape: Bool) -> UIEdgeInsets { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0) default: return UIEdgeInsets.zero @@ -166,7 +182,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return inLandscape ? 21.0 : 34.0 case .iPadPro3rdGen, .iPadPro11Inch: return 21.0 @@ -198,7 +214,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { var statusBarHeight: CGFloat { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 44.0 case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: return 24.0 @@ -216,7 +232,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 162.0 case .iPhone6, .iPhone6Plus: return 163.0 - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 172.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 348.0 @@ -235,9 +251,9 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 216.0 case .iPhone6Plus: return 226.0 - case .iPhoneX, .iPhone12Mini, .iPhone12: - return 291.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro: + return 292.0 + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax: return 302.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 263.0 @@ -256,7 +272,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func predictiveInputHeight(inLandscape: Bool) -> CGFloat { if inLandscape { switch self { - case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax: + case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 37.0 case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: return 50.0 @@ -267,7 +283,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { switch self { case .iPhone4, .iPhone5: return 37.0 - case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax: + case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 44.0 case .iPhone6Plus: return 45.0 diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 99bfdc5ac8..1eae1fcc89 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -12,7 +12,7 @@ open class SparseNode: ASDisplayNode { if !self.bounds.contains(point) { return nil } - for view in self.view.subviews { + for view in self.view.subviews.reversed() { if let result = view.hitTest(self.view.convert(point, to: view), with: event), result.isUserInteractionEnabled { return result } @@ -27,6 +27,26 @@ open class SparseNode: ASDisplayNode { } } +open class SparseContainerView: UIView { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.alpha.isZero { + return nil + } + for view in self.subviews.reversed() { + if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + return result + } + } + + let result = super.hitTest(point, with: event) + if result != self { + return result + } else { + return nil + } + } +} + public final class NavigationBarTheme { public static func generateBackArrowImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 13.0, height: 22.0), rotatedContext: { size, context in @@ -266,6 +286,146 @@ public final class NavigationBackgroundNode: ASDisplayNode { } } +open class BlurredBackgroundView: UIView { + private var _color: UIColor + + private var enableBlur: Bool + + private var effectView: UIVisualEffectView? + private let backgroundView: UIView + + private var validLayout: (CGSize, CGFloat)? + + public var backgroundCornerRadius: CGFloat { + if let (_, cornerRadius) = self.validLayout { + return cornerRadius + } else { + return 0.0 + } + } + + public init(color: UIColor, enableBlur: Bool = true) { + self._color = .clear + self.enableBlur = enableBlur + + self.backgroundView = UIView() + + super.init(frame: CGRect()) + + self.addSubview(self.backgroundView) + + self.updateColor(color: color, transition: .immediate) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateBackgroundBlur(forceKeepBlur: Bool) { + if self.enableBlur && !sharedIsReduceTransparencyEnabled && ((self._color.alpha > .ulpOfOne && self._color.alpha < 0.95) || forceKeepBlur) { + if self.effectView == nil { + let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + + for subview in effectView.subviews { + if subview.description.contains("VisualEffectSubview") { + subview.isHidden = true + } + } + + if let sublayer = effectView.layer.sublayers?[0], let filters = sublayer.filters { + sublayer.backgroundColor = nil + sublayer.isOpaque = false + let allowedKeys: [String] = [ + "colorSaturate", + "gaussianBlur" + ] + sublayer.filters = filters.filter { filter in + guard let filter = filter as? NSObject else { + return true + } + let filterName = String(describing: filter) + if !allowedKeys.contains(filterName) { + return false + } + return true + } + } + + if let (size, cornerRadius) = self.validLayout { + effectView.frame = CGRect(origin: CGPoint(), size: size) + ContainedViewLayoutTransition.immediate.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius) + effectView.clipsToBounds = !cornerRadius.isZero + } + self.effectView = effectView + self.insertSubview(effectView, at: 0) + } + } else if let effectView = self.effectView { + self.effectView = nil + effectView.removeFromSuperview() + } + } + + public func updateColor(color: UIColor, enableBlur: Bool? = nil, forceKeepBlur: Bool = false, transition: ContainedViewLayoutTransition) { + let effectiveEnableBlur = enableBlur ?? self.enableBlur + + if self._color.isEqual(color) && self.enableBlur == effectiveEnableBlur { + return + } + self._color = color + self.enableBlur = effectiveEnableBlur + + if sharedIsReduceTransparencyEnabled { + transition.updateBackgroundColor(layer: self.backgroundView.layer, color: self._color.withAlphaComponent(1.0)) + } else { + transition.updateBackgroundColor(layer: self.backgroundView.layer, color: self._color) + } + + self.updateBackgroundBlur(forceKeepBlur: forceKeepBlur) + } + + public func update(size: CGSize, cornerRadius: CGFloat = 0.0, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, cornerRadius) + + let contentFrame = CGRect(origin: CGPoint(), size: size) + transition.updateFrame(view: self.backgroundView, frame: contentFrame, beginWithCurrentState: true) + if let effectView = self.effectView, effectView.frame != contentFrame { + transition.updateFrame(layer: effectView.layer, frame: contentFrame, beginWithCurrentState: true) + if let sublayers = effectView.layer.sublayers { + for sublayer in sublayers { + transition.updateFrame(layer: sublayer, frame: contentFrame, beginWithCurrentState: true) + } + } + } + + transition.updateCornerRadius(layer: self.backgroundView.layer, cornerRadius: cornerRadius) + if let effectView = self.effectView { + transition.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius) + effectView.clipsToBounds = !cornerRadius.isZero + } + } + + public func update(size: CGSize, cornerRadius: CGFloat = 0.0, animator: ControlledTransitionAnimator) { + self.validLayout = (size, cornerRadius) + + let contentFrame = CGRect(origin: CGPoint(), size: size) + animator.updateFrame(layer: self.backgroundView.layer, frame: contentFrame, completion: nil) + if let effectView = self.effectView, effectView.frame != contentFrame { + animator.updateFrame(layer: effectView.layer, frame: contentFrame, completion: nil) + if let sublayers = effectView.layer.sublayers { + for sublayer in sublayers { + animator.updateFrame(layer: sublayer, frame: contentFrame, completion: nil) + } + } + } + + animator.updateCornerRadius(layer: self.backgroundView.layer, cornerRadius: cornerRadius, completion: nil) + if let effectView = self.effectView { + animator.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius, completion: nil) + effectView.clipsToBounds = !cornerRadius.isZero + } + } +} + open class NavigationBar: ASDisplayNode { public static var defaultSecondaryContentHeight: CGFloat { return 38.0 diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index 2b6ee6a68e..3592dc0062 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -483,6 +483,38 @@ private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? { return view } +private func makeLayerSubtreeSnapshotAsView(layer: CALayer) -> UIView? { + if layer is AVSampleBufferDisplayLayer { + return nil + } + let view = UIView() + view.layer.isHidden = layer.isHidden + view.layer.opacity = layer.opacity + view.layer.contents = layer.contents + view.layer.contentsRect = layer.contentsRect + view.layer.contentsScale = layer.contentsScale + view.layer.contentsCenter = layer.contentsCenter + view.layer.contentsGravity = layer.contentsGravity + view.layer.masksToBounds = layer.masksToBounds + view.layer.cornerRadius = layer.cornerRadius + view.layer.backgroundColor = layer.backgroundColor + if let sublayers = layer.sublayers { + for sublayer in sublayers { + let subtree = makeLayerSubtreeSnapshotAsView(layer: sublayer) + if let subtree = subtree { + subtree.layer.transform = sublayer.transform + subtree.layer.frame = sublayer.frame + subtree.layer.bounds = sublayer.bounds + view.addSubview(subtree) + } else { + return nil + } + } + } + return view +} + + public extension UIView { func snapshotContentTree(unhide: Bool = false, keepTransform: Bool = false) -> UIView? { let wasHidden = self.isHidden @@ -523,6 +555,26 @@ public extension CALayer { } } +public extension CALayer { + func snapshotContentTreeAsView(unhide: Bool = false) -> UIView? { + let wasHidden = self.isHidden + if unhide && wasHidden { + self.isHidden = false + } + let snapshot = makeLayerSubtreeSnapshotAsView(layer: self) + if unhide && wasHidden { + self.isHidden = true + } + if let snapshot = snapshot { + snapshot.frame = self.frame + snapshot.bounds = self.bounds + return snapshot + } + + return nil + } +} + public extension CGRect { var topLeft: CGPoint { return self.origin diff --git a/submodules/FetchManagerImpl/BUILD b/submodules/FetchManagerImpl/BUILD index 93a7be74c7..2838c3104c 100644 --- a/submodules/FetchManagerImpl/BUILD +++ b/submodules/FetchManagerImpl/BUILD @@ -16,6 +16,7 @@ swift_library( "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/AccountContext:AccountContext", "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/Utils/RangeSet:RangeSet", ], visibility = [ "//visibility:public", diff --git a/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift b/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift index 7308111946..c998542496 100644 --- a/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift +++ b/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift @@ -6,6 +6,7 @@ import Postbox import TelegramUIPreferences import AccountContext import UniversalMediaPlayer +import RangeSet public struct FetchManagerLocationEntryId: Hashable { public let location: FetchManagerLocation @@ -41,15 +42,15 @@ private final class FetchManagerLocationEntry { var userInitiated: Bool = false var storeToDownloadsPeerType: MediaAutoDownloadPeerType? let references = Bag() - let ranges = Bag() + let ranges = Bag>() var elevatedPriorityReferenceCount: Int32 = 0 var userInitiatedPriorityIndices: [Int32] = [] var isPaused: Bool = false - var combinedRanges: IndexSet { - var result = IndexSet() + var combinedRanges: RangeSet { + var result = RangeSet() if self.userInitiated { - result.insert(integersIn: 0 ..< Int(Int64.max)) + result.insert(contentsOf: 0 ..< Int64.max) } else { for range in self.ranges.copyItems() { result.formUnion(range) @@ -77,7 +78,7 @@ private final class FetchManagerLocationEntry { private final class FetchManagerActiveContext { let userInitiated: Bool - var ranges = IndexSet() + var ranges = RangeSet() var disposable: Disposable? init(userInitiated: Bool) { @@ -224,12 +225,13 @@ private final class FetchManagerCategoryContext { let entryCompleted = self.entryCompleted let storeManager = self.storeManager let parsedRanges: [(Range, MediaBoxFetchPriority)]? - if ranges.count == 1 && ranges.min() == 0 && ranges.max() == Int(Int64.max) { + + if ranges == RangeSet(0 ..< Int64.max) { parsedRanges = nil } else { var resultRanges: [(Range, MediaBoxFetchPriority)] = [] - for range in ranges.rangeView { - resultRanges.append((Int64(range.lowerBound) ..< Int64(range.upperBound), .default)) + for range in ranges.ranges { + resultRanges.append((range, .default)) } parsedRanges = resultRanges } @@ -312,14 +314,14 @@ private final class FetchManagerCategoryContext { var count = 0 var isCompleteRange = false var isVideoPreload = false - for range in ranges.rangeView { + for range in ranges.ranges { count += 1 - if range.lowerBound == 0 && range.upperBound == Int(Int64.max) { + if range.lowerBound == 0 && range.upperBound == Int64.max { isCompleteRange = true } } - if count == 2, let range = ranges.rangeView.first, range.lowerBound == 0 && range.upperBound == 2 * 1024 * 1024 { + if count == 2, let range = ranges.ranges.first, range.lowerBound == 0 && range.upperBound == 2 * 1024 * 1024 { isVideoPreload = true } @@ -327,8 +329,8 @@ private final class FetchManagerCategoryContext { parsedRanges = nil } else { var resultRanges: [(Range, MediaBoxFetchPriority)] = [] - for range in ranges.rangeView { - resultRanges.append((Int64(range.lowerBound) ..< Int64(range.upperBound), .default)) + for range in ranges.ranges { + resultRanges.append((range, .default)) } parsedRanges = resultRanges } @@ -709,7 +711,7 @@ public final class FetchManagerImpl: FetchManager { } } - public func interactivelyFetched(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, mediaReference: AnyMediaReference?, resourceReference: MediaResourceReference, ranges: IndexSet, statsCategory: MediaResourceStatsCategory, elevatedPriority: Bool, userInitiated: Bool, priority: FetchManagerPriority = .userInitiated, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal { + public func interactivelyFetched(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, mediaReference: AnyMediaReference?, resourceReference: MediaResourceReference, ranges: RangeSet, statsCategory: MediaResourceStatsCategory, elevatedPriority: Bool, userInitiated: Bool, priority: FetchManagerPriority = .userInitiated, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal { let queue = self.queue return Signal { [weak self] subscriber in if let strongSelf = self { diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift index bb935c0295..14159d8e10 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift @@ -178,7 +178,7 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi let animation = contentNode.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.25) animation.fillMode = .both if !fastOut { - animation.beginTime = CACurrentMediaTime() + 0.1 + animation.beginTime = contentNode.layer.convertTime(CACurrentMediaTime(), from: nil) + 0.1 } contentNode.layer.add(animation, forKey: "opacity") diff --git a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift index 4e2d372bb9..eabbb8e37a 100644 --- a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift +++ b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift @@ -199,7 +199,7 @@ final class LanguageLinkPreviewControllerNode: ViewControllerTracingNode, UIScro let animation = contentNode.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.35) animation.fillMode = .both if !fastOut { - animation.beginTime = CACurrentMediaTime() + 0.1 + animation.beginTime = contentNode.layer.convertTime(CACurrentMediaTime(), from: nil) + 0.1 } contentNode.layer.add(animation, forKey: "opacity") diff --git a/submodules/ManagedFile/Sources/ManagedFile.swift b/submodules/ManagedFile/Sources/ManagedFile.swift index d43ab46369..ba5c4a5cbf 100644 --- a/submodules/ManagedFile/Sources/ManagedFile.swift +++ b/submodules/ManagedFile/Sources/ManagedFile.swift @@ -19,6 +19,7 @@ public final class ManagedFile { private let queue: Queue? private let fd: Int32 private let mode: Mode + private var isClosed: Bool = false public init?(queue: Queue?, path: String, mode: Mode) { if let queue = queue { @@ -48,16 +49,27 @@ public final class ManagedFile { } deinit { + if let queue = self.queue { + assert(queue.isCurrent()) + } + if !self.isClosed { + close(self.fd) + } + } + + public func _unsafeClose() { if let queue = self.queue { assert(queue.isCurrent()) } close(self.fd) + self.isClosed = true } public func write(_ data: UnsafeRawPointer, count: Int) -> Int { if let queue = self.queue { assert(queue.isCurrent()) } + assert(!self.isClosed) return wrappedWrite(self.fd, data, count) } @@ -65,6 +77,7 @@ public final class ManagedFile { if let queue = self.queue { assert(queue.isCurrent()) } + assert(!self.isClosed) return wrappedRead(self.fd, data, count) } @@ -72,6 +85,7 @@ public final class ManagedFile { if let queue = self.queue { assert(queue.isCurrent()) } + assert(!self.isClosed) var result = Data(count: count) result.withUnsafeMutableBytes { buffer -> Void in guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { @@ -87,6 +101,7 @@ public final class ManagedFile { if let queue = self.queue { assert(queue.isCurrent()) } + assert(!self.isClosed) lseek(self.fd, position, SEEK_SET) } @@ -94,6 +109,7 @@ public final class ManagedFile { if let queue = self.queue { assert(queue.isCurrent()) } + assert(!self.isClosed) ftruncate(self.fd, count) } @@ -101,6 +117,7 @@ public final class ManagedFile { if let queue = self.queue { assert(queue.isCurrent()) } + assert(!self.isClosed) var value = stat() if fstat(self.fd, &value) == 0 { return value.st_size @@ -113,6 +130,7 @@ public final class ManagedFile { if let queue = self.queue { assert(queue.isCurrent()) } + assert(!self.isClosed) return lseek(self.fd, 0, SEEK_CUR); } @@ -121,6 +139,7 @@ public final class ManagedFile { if let queue = self.queue { assert(queue.isCurrent()) } + assert(!self.isClosed) fsync(self.fd) } } diff --git a/submodules/PasscodeUI/Sources/PasscodeLayout.swift b/submodules/PasscodeUI/Sources/PasscodeLayout.swift index 7fb5e8f78b..bd76dad5b8 100644 --- a/submodules/PasscodeUI/Sources/PasscodeLayout.swift +++ b/submodules/PasscodeUI/Sources/PasscodeLayout.swift @@ -67,7 +67,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 226.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneX, .iPhone12Mini, .iPhone12: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro: self.buttonSize = 75.0 self.horizontalSecond = 103.0 self.horizontalThird = 206.0 @@ -78,7 +78,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 294.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax: self.buttonSize = 85.0 self.horizontalSecond = 115.0 self.horizontalThird = 230.0 @@ -151,11 +151,11 @@ public struct PasscodeLayout { self.titleOffset = 112.0 self.subtitleOffset = -6.0 self.inputFieldOffset = 156.0 - case .iPhoneX, .iPhone12Mini, .iPhone12: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro: self.titleOffset = 162.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 206.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax: self.titleOffset = 180.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 226.0 diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 341b196a8b..f1cbdb1419 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -188,7 +188,8 @@ public final class MediaBox { self.timeBasedCleanup = TimeBasedCleanup(generalPaths: [ self.basePath, - self.basePath + "/cache" + self.basePath + "/cache", + self.basePath + "/animation-cache" ], shortLivedPaths: [ self.basePath + "/short-cache" ]) @@ -1238,6 +1239,27 @@ public final class MediaBox { } } + func processRecursive(directoryPath: String, subdirectoryPath: String) { + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: directoryPath), includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let prefix = url.lastPathComponent.components(separatedBy: ":").first, excludePrefixes.contains(prefix) { + continue loop + } + + if let isDirectory = (try? url.resourceValues(forKeys: Set([.isDirectoryKey])))?.isDirectory, isDirectory { + processRecursive(directoryPath: url.path, subdirectoryPath: subdirectoryPath + "/\(url.lastPathComponent)") + } else if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + paths.append("\(subdirectoryPath)/" + url.lastPathComponent) + cacheResult += Int64(value) + } + } + } + } + } + + processRecursive(directoryPath: self.basePath + "/animation-cache", subdirectoryPath: "animation-cache") + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath + "/short-cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { loop: for url in enumerator { if let url = url as? URL { diff --git a/submodules/Postbox/Sources/MediaBoxFile.swift b/submodules/Postbox/Sources/MediaBoxFile.swift index 578f1a82b2..aeb69d5829 100644 --- a/submodules/Postbox/Sources/MediaBoxFile.swift +++ b/submodules/Postbox/Sources/MediaBoxFile.swift @@ -108,7 +108,7 @@ private final class MediaBoxFileMap { return nil } - if count < 0 || length < 4 + 4 + count * 2 * 4 { + if count < 0 || UInt64(length) < 4 + 4 + UInt64(count) * 2 * 4 { return nil } diff --git a/submodules/SoftwareVideo/Sources/SampleBufferPool.swift b/submodules/SoftwareVideo/Sources/SampleBufferPool.swift index 1bc235682e..da15e330a1 100644 --- a/submodules/SoftwareVideo/Sources/SampleBufferPool.swift +++ b/submodules/SoftwareVideo/Sources/SampleBufferPool.swift @@ -18,7 +18,6 @@ public final class SampleBufferLayer { public let layer: AVSampleBufferDisplayLayer private let enqueue: (AVSampleBufferDisplayLayer) -> Void - public var isFreed: Bool = false fileprivate init(layer: AVSampleBufferDisplayLayer, enqueue: @escaping (AVSampleBufferDisplayLayer) -> Void) { self.layer = layer diff --git a/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift index e085159f60..fd3ab89d89 100644 --- a/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift +++ b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift @@ -5,6 +5,7 @@ import TelegramCore import SwiftSignalKit import CoreMedia import UniversalMediaPlayer +import AVFoundation public let softwareVideoApplyQueue = Queue() public let softwareVideoWorkers = ThreadPool(threadCount: 3, threadPriority: 0.2) @@ -25,7 +26,8 @@ public final class SoftwareVideoLayerFrameManager { private let resource: MediaResource private let secondaryResource: MediaResource? private let queue: ThreadPoolQueue - private let layerHolder: SampleBufferLayer + private let layerHolder: SampleBufferLayer? + private weak var layer: AVSampleBufferDisplayLayer? private var rotationAngle: CGFloat = 0.0 private var aspect: CGFloat = 1.0 @@ -33,9 +35,9 @@ public final class SoftwareVideoLayerFrameManager { private var layerRotationAngleAndAspect: (CGFloat, CGFloat)? private var didStart = false - var started: () -> Void = { } + public var started: () -> Void = { } - public init(account: Account, fileReference: FileMediaReference, layerHolder: SampleBufferLayer, hintVP9: Bool = false) { + public init(account: Account, fileReference: FileMediaReference, layerHolder: SampleBufferLayer?, layer: AVSampleBufferDisplayLayer? = nil, hintVP9: Bool = false) { var resource = fileReference.media.resource var secondaryResource: MediaResource? for attribute in fileReference.media.attributes { @@ -54,8 +56,9 @@ public final class SoftwareVideoLayerFrameManager { self.secondaryResource = secondaryResource self.queue = ThreadPoolQueue(threadPool: softwareVideoWorkers) self.layerHolder = layerHolder - layerHolder.layer.videoGravity = .resizeAspectFill - layerHolder.layer.masksToBounds = true + self.layer = layer ?? layerHolder?.layer + self.layer?.videoGravity = .resizeAspectFill + self.layer?.masksToBounds = true self.fetchDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(resource)).start() } @@ -139,8 +142,8 @@ public final class SoftwareVideoLayerFrameManager { for i in (0 ... latestFrameIndex).reversed() { self.frames.remove(at: i) } - if self.layerHolder.layer.status == .failed { - self.layerHolder.layer.flush() + if self.layer?.status == .failed { + self.layer?.flush() } /*if self.layerRotationAngleAndAspect?.0 != self.rotationAngle || self.layerRotationAngleAndAspect?.1 != self.aspect { self.layerRotationAngleAndAspect = (self.rotationAngle, self.aspect) @@ -150,7 +153,7 @@ public final class SoftwareVideoLayerFrameManager { } self.layerHolder.layer.setAffineTransform(transform) }*/ - self.layerHolder.layer.enqueue(frame.sampleBuffer) + self.layer?.enqueue(frame.sampleBuffer) if !self.didStart { self.didStart = true diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift index 3c598121cf..482a6dc37c 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift @@ -48,12 +48,12 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - public var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? { + public var sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? { didSet { if self.isNodeLoaded { if let sendSticker = self.sendSticker { - self.controllerNode.sendSticker = { [weak self] file, sourceNode, sourceRect in - if sendSticker(file, sourceNode, sourceRect) { + self.controllerNode.sendSticker = { [weak self] file, sourceView, sourceRect in + if sendSticker(file, sourceView, sourceRect) { self?.dismiss() return true } else { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift index c5bcd464cb..a317390966 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift @@ -73,7 +73,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol var presentInGlobalOverlay: ((ViewController, Any?) -> Void)? var dismiss: (() -> Void)? var cancel: (() -> Void)? - var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + var sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let actionPerformed: ((StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) -> Void)? let ready = Promise() @@ -220,9 +220,9 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol menuItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode, animationNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode, imageNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode.view, imageNode.bounds) } } f(.default) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 04b7b114c5..7cb8c4b2b2 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -85,7 +85,7 @@ private final class StickerPackContainer: ASDisplayNode { private let decideNextAction: (StickerPackContainer, StickerPackAction) -> StickerPackNextAction private let requestDismiss: () -> Void private let presentInGlobalOverlay: (ViewController, Any?) -> Void - private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let backgroundNode: ASImageNode private let gridNode: GridNode private let actionAreaBackgroundNode: NavigationBackgroundNode @@ -129,7 +129,7 @@ private final class StickerPackContainer: ASDisplayNode { private weak var peekController: PeekController? - init(index: Int, context: AccountContext, presentationData: PresentationData, stickerPack: StickerPackReference, decideNextAction: @escaping (StickerPackContainer, StickerPackAction) -> StickerPackNextAction, requestDismiss: @escaping () -> Void, expandProgressUpdated: @escaping (StickerPackContainer, ContainedViewLayoutTransition, ContainedViewLayoutTransition) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, openMention: @escaping (String) -> Void, controller: StickerPackScreenImpl?) { + init(index: Int, context: AccountContext, presentationData: PresentationData, stickerPack: StickerPackReference, decideNextAction: @escaping (StickerPackContainer, StickerPackAction) -> StickerPackNextAction, requestDismiss: @escaping () -> Void, expandProgressUpdated: @escaping (StickerPackContainer, ContainedViewLayoutTransition, ContainedViewLayoutTransition) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, openMention: @escaping (String) -> Void, controller: StickerPackScreenImpl?) { self.index = index self.context = context self.controller = controller @@ -367,9 +367,9 @@ private final class StickerPackContainer: ASDisplayNode { menuItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode, animationNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode, imageNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode.view, imageNode.bounds) } } f(.default) @@ -964,7 +964,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { private let modalProgressUpdated: (CGFloat, ContainedViewLayoutTransition) -> Void private let dismissed: () -> Void private let presentInGlobalOverlay: (ViewController, Any?) -> Void - private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let openMention: (String) -> Void private let dimNode: ASDisplayNode @@ -986,7 +986,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { var onReady: () -> Void = {} var onError: () -> Void = {} - init(context: AccountContext, controller: StickerPackScreenImpl, stickerPacks: [StickerPackReference], initialSelectedStickerPackIndex: Int, modalProgressUpdated: @escaping (CGFloat, ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, openMention: @escaping (String) -> Void) { + init(context: AccountContext, controller: StickerPackScreenImpl, stickerPacks: [StickerPackReference], initialSelectedStickerPackIndex: Int, modalProgressUpdated: @escaping (CGFloat, ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, openMention: @escaping (String) -> Void) { self.context = context self.controller = controller self.presentationData = controller.presentationData @@ -1302,7 +1302,7 @@ public final class StickerPackScreenImpl: ViewController { private let stickerPacks: [StickerPackReference] private let initialSelectedStickerPackIndex: Int fileprivate weak var parentNavigationController: NavigationController? - private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private var controllerNode: StickerPackScreenNode { return self.displayNode as! StickerPackScreenNode @@ -1320,7 +1320,7 @@ public final class StickerPackScreenImpl: ViewController { private var alreadyDidAppear: Bool = false - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, stickerPacks: [StickerPackReference], selectedStickerPackIndex: Int = 0, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) -> Void)? = nil) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, stickerPacks: [StickerPackReference], selectedStickerPackIndex: Int = 0, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) -> Void)? = nil) { self.context = context self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.stickerPacks = stickerPacks @@ -1482,7 +1482,7 @@ public enum StickerPackScreenPerformedAction { case remove(positionInList: Int) } -public func StickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: StickerPackPreviewControllerMode = .default, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) -> Void)? = nil, dismissed: (() -> Void)? = nil) -> ViewController { +public func StickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: StickerPackPreviewControllerMode = .default, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) -> Void)? = nil, dismissed: (() -> Void)? = nil) -> ViewController { let stickerPacks = [mainStickerPack] let controller = StickerPackScreenImpl(context: context, stickerPacks: stickerPacks, selectedStickerPackIndex: stickerPacks.firstIndex(of: mainStickerPack) ?? 0, parentNavigationController: parentNavigationController, sendSticker: sendSticker, actionPerformed: actionPerformed) controller.dismissed = dismissed diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 4f77dde770..560e7fde82 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -433,6 +433,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1827637959] = { return Api.MessageEntity.parse_messageEntityBotCommand($0) } dict[1280209983] = { return Api.MessageEntity.parse_messageEntityCashtag($0) } dict[681706865] = { return Api.MessageEntity.parse_messageEntityCode($0) } + dict[-727707947] = { return Api.MessageEntity.parse_messageEntityCustomEmoji($0) } dict[1692693954] = { return Api.MessageEntity.parse_messageEntityEmail($0) } dict[1868782349] = { return Api.MessageEntity.parse_messageEntityHashtag($0) } dict[-2106619040] = { return Api.MessageEntity.parse_messageEntityItalic($0) } diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 8172159a5c..184426636f 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -7,6 +7,7 @@ public extension Api { case messageEntityBotCommand(offset: Int32, length: Int32) case messageEntityCashtag(offset: Int32, length: Int32) case messageEntityCode(offset: Int32, length: Int32) + case messageEntityCustomEmoji(offset: Int32, length: Int32, stickerset: Api.InputStickerSet, documentId: Int64) case messageEntityEmail(offset: Int32, length: Int32) case messageEntityHashtag(offset: Int32, length: Int32) case messageEntityItalic(offset: Int32, length: Int32) @@ -73,6 +74,15 @@ public extension Api { serializeInt32(offset, buffer: buffer, boxed: false) serializeInt32(length, buffer: buffer, boxed: false) break + case .messageEntityCustomEmoji(let offset, let length, let stickerset, let documentId): + if boxed { + buffer.appendInt32(-727707947) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + stickerset.serialize(buffer, true) + serializeInt64(documentId, buffer: buffer, boxed: false) + break case .messageEntityEmail(let offset, let length): if boxed { buffer.appendInt32(1692693954) @@ -186,6 +196,8 @@ public extension Api { return ("messageEntityCashtag", [("offset", String(describing: offset)), ("length", String(describing: length))]) case .messageEntityCode(let offset, let length): return ("messageEntityCode", [("offset", String(describing: offset)), ("length", String(describing: length))]) + case .messageEntityCustomEmoji(let offset, let length, let stickerset, let documentId): + return ("messageEntityCustomEmoji", [("offset", String(describing: offset)), ("length", String(describing: length)), ("stickerset", String(describing: stickerset)), ("documentId", String(describing: documentId))]) case .messageEntityEmail(let offset, let length): return ("messageEntityEmail", [("offset", String(describing: offset)), ("length", String(describing: length))]) case .messageEntityHashtag(let offset, let length): @@ -318,6 +330,28 @@ public extension Api { return nil } } + public static func parse_messageEntityCustomEmoji(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.InputStickerSet? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.InputStickerSet + } + var _4: Int64? + _4 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.MessageEntity.messageEntityCustomEmoji(offset: _1!, length: _2!, stickerset: _3!, documentId: _4!) + } + else { + return nil + } + } public static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift index 8019bfe7a7..7e3d8915e3 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift @@ -362,7 +362,7 @@ public final class VoiceChatJoinScreen: ViewController { let animation = contentNode.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.35) animation.fillMode = .both if !fastOut { - animation.beginTime = CACurrentMediaTime() + 0.1 + animation.beginTime = contentNode.layer.convertTime(CACurrentMediaTime(), from: nil) + 0.1 } contentNode.layer.add(animation, forKey: "opacity") diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 12a65b62e0..536de15183 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -374,8 +374,10 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BankCard)) case let .messageEntitySpoiler(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Spoiler)) - /*case let .messageEntityAnimatedEmoji(offset, length): - result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .AnimatedEmoji(nil)))*/ + case let .messageEntityCustomEmoji(offset, length, stickerset, documentId): + if let stickerPack = StickerPackReference(apiInputSet: stickerset) { + result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .CustomEmoji(stickerPack: stickerPack, fileId: documentId))) + } } } return result diff --git a/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift index 25da7187cb..6c3e4b7dcb 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift @@ -48,9 +48,8 @@ func apiEntitiesFromMessageTextEntities(_ entities: [MessageTextEntity], associa apiEntities.append(.messageEntityBankCard(offset: offset, length: length)) case .Spoiler: apiEntities.append(.messageEntitySpoiler(offset: offset, length: length)) - case .AnimatedEmoji: - //apiEntities.append(.messageEntityAnimatedEmoji(offset: offset, length: length)) - break + case let .CustomEmoji(stickerPack, fileId): + apiEntities.append(.messageEntityCustomEmoji(offset: offset, length: length, stickerset: stickerPack.apiInputStickerSet, documentId: fileId)) case .Custom: break } diff --git a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift index 8cd7216db5..50b56a3a48 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift @@ -715,7 +715,7 @@ private func decryptedEntities73(_ entities: [MessageTextEntity]?) -> [SecretApi break case .Spoiler: break - case .AnimatedEmoji: + case .CustomEmoji: break case .Custom: break @@ -768,7 +768,7 @@ private func decryptedEntities101(_ entities: [MessageTextEntity]?) -> [SecretAp break case .Spoiler: break - case .AnimatedEmoji: + case .CustomEmoji: break case .Custom: break diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index f0ea775946..dc83c6cd28 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 143 + return 144 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index c01106354f..64a55fe83e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -12,7 +12,7 @@ private let typeHintFileIsLarge: Int32 = 7 private let typeHintIsValidated: Int32 = 8 private let typeNoPremium: Int32 = 9 -public enum StickerPackReference: PostboxCoding, Hashable, Equatable { +public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable { case id(id: Int64, accessHash: Int64) case name(String) case animatedEmoji @@ -37,6 +37,27 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable { } } + public init(decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + let discriminator = try container.decode(Int32.self, forKey: "r") + switch discriminator { + case 0: + self = .id(id: try container.decode(Int64.self, forKey: "i"), accessHash: try container.decode(Int64.self, forKey: "h")) + case 1: + self = .name(try container.decode(String.self, forKey: "n")) + case 2: + self = .animatedEmoji + case 3: + self = .dice((try? container.decode(String.self, forKey: "e")) ?? "🎲") + case 4: + self = .animatedEmojiAnimations + default: + self = .name("") + assertionFailure() + } + } + public func encode(_ encoder: PostboxEncoder) { switch self { case let .id(id, accessHash): @@ -56,6 +77,27 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable { } } + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + switch self { + case let .id(id, accessHash): + try container.encode(0 as Int32, forKey: "r") + try container.encode(id, forKey: "i") + try container.encode(accessHash, forKey: "h") + case let .name(name): + try container.encode(1 as Int32, forKey: "r") + try container.encode(name, forKey: "n") + case .animatedEmoji: + try container.encode(2 as Int32, forKey: "r") + case let .dice(emoji): + try container.encode(3 as Int32, forKey: "r") + try container.encode(emoji, forKey: "e") + case .animatedEmojiAnimations: + try container.encode(4 as Int32, forKey: "r") + } + } + public static func ==(lhs: StickerPackReference, rhs: StickerPackReference) -> Bool { switch lhs { case let .id(id, accessHash): diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift index 6906d5ba9f..7c6bbe7d3d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift @@ -21,7 +21,7 @@ public enum MessageTextEntityType: Equatable { case Underline case BankCard case Spoiler - case AnimatedEmoji(MediaId?) + case CustomEmoji(stickerPack: StickerPackReference, fileId: Int64) case Custom(type: CustomEntityType) } @@ -73,7 +73,11 @@ public struct MessageTextEntity: PostboxCoding, Codable, Equatable { case 17: self.type = .Spoiler case 18: - self.type = .AnimatedEmoji(decoder.decodeObjectForKey("mediaId") as? MediaId) + if let stickerPack = decoder.decodeObjectForKey("s", decoder: { StickerPackReference(decoder: $0) }) as? StickerPackReference { + self.type = .CustomEmoji(stickerPack: stickerPack, fileId: decoder.decodeInt64ForKey("f", orElse: 0)) + } else { + self.type = .Unknown + } case Int32.max: self.type = .Custom(type: decoder.decodeInt32ForKey("type", orElse: 0)) default: @@ -130,7 +134,7 @@ public struct MessageTextEntity: PostboxCoding, Codable, Equatable { case 17: self.type = .Spoiler case 18: - self.type = .AnimatedEmoji(try? container.decode(MediaId.self, forKey: "mediaId")) + self.type = .CustomEmoji(stickerPack: try container.decode(StickerPackReference.self, forKey: "s"), fileId: try container.decode(Int64.self, forKey: "f")) case Int32.max: let customType: Int32 = (try? container.decode(Int32.self, forKey: "type")) ?? 0 self.type = .Custom(type: customType) @@ -181,13 +185,10 @@ public struct MessageTextEntity: PostboxCoding, Codable, Equatable { encoder.encodeInt32(16, forKey: "_rawValue") case .Spoiler: encoder.encodeInt32(17, forKey: "_rawValue") - case let .AnimatedEmoji(mediaId): + case let .CustomEmoji(stickerPack, fileId): encoder.encodeInt32(18, forKey: "_rawValue") - if let mediaId = mediaId { - encoder.encodeObject(mediaId, forKey: "mediaId") - } else { - encoder.encodeNil(forKey: "mediaId") - } + encoder.encodeObject(stickerPack, forKey: "s") + encoder.encodeInt64(fileId, forKey: "f") case let .Custom(type): encoder.encodeInt32(Int32.max, forKey: "_rawValue") encoder.encodeInt32(type, forKey: "type") @@ -238,11 +239,10 @@ public struct MessageTextEntity: PostboxCoding, Codable, Equatable { try container.encode(16 as Int32, forKey: "_rawValue") case .Spoiler: try container.encode(17 as Int32, forKey: "_rawValue") - case let .AnimatedEmoji(mediaId): + case let .CustomEmoji(stickerPack, fileId): try container.encode(18 as Int32, forKey: "_rawValue") - if let mediaId = mediaId { - try container.encode(mediaId, forKey: "mediaId") - } + try container.encode(stickerPack, forKey: "s") + try container.encode(fileId, forKey: "f") case let .Custom(type): try container.encode(Int32.max as Int32, forKey: "_rawValue") try container.encode(type as Int32, forKey: "type") diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 22e7126343..ccf6ae5604 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -282,10 +282,12 @@ swift_library( "//submodules/TelegramUI/Components/AudioWaveformComponent:AudioWaveformComponent", "//submodules/TelegramUI/Components/EditableChatTextNode:EditableChatTextNode", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", - "//submodules/TelegramUI/Components/EmojiKeyboard:EmojiKeyboard", + "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", + "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC", "//submodules/Media/LocalAudioTranscription:LocalAudioTranscription", ] + select({ diff --git a/submodules/TelegramUI/Components/AnimationCache/BUILD b/submodules/TelegramUI/Components/AnimationCache/BUILD index c5773d4849..3e79cc1f13 100644 --- a/submodules/TelegramUI/Components/AnimationCache/BUILD +++ b/submodules/TelegramUI/Components/AnimationCache/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/CryptoUtils:CryptoUtils", "//submodules/ManagedFile:ManagedFile", + "//submodules/TelegramUI/Components/AnimationCache/DCT:DCT", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AnimationCache/DCT/BUILD b/submodules/TelegramUI/Components/AnimationCache/DCT/BUILD new file mode 100644 index 0000000000..f98db65bc2 --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/DCT/BUILD @@ -0,0 +1,23 @@ + +objc_library( + name = "DCT", + enable_modules = True, + module_name = "DCT", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.h", + ]), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + sdk_frameworks = [ + "Foundation", + "Accelerate", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AnimationCache/DCT/PublicHeaders/DCT/DCT.h b/submodules/TelegramUI/Components/AnimationCache/DCT/PublicHeaders/DCT/DCT.h new file mode 100644 index 0000000000..01fa6dc26c --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/DCT/PublicHeaders/DCT/DCT.h @@ -0,0 +1,14 @@ +#ifndef DctImageTransform_h +#define DctImageTransform_h + +#import + +#import + +NSData *generateForwardDctData(int quality); +void performForwardDct(uint8_t const *pixels, int16_t *coefficients, int width, int height, int bytesPerRow, NSData *dctData); + +NSData *generateInverseDctData(int quality); +void performInverseDct(int16_t const *coefficients, uint8_t *pixels, int width, int height, int coefficientsPerRow, int bytesPerRow, NSData *idctData); + +#endif /* DctImageTransform_h */ diff --git a/submodules/TelegramUI/Components/AnimationCache/DCT/PublicHeaders/DCT/YuvConversion.h b/submodules/TelegramUI/Components/AnimationCache/DCT/PublicHeaders/DCT/YuvConversion.h new file mode 100644 index 0000000000..a7ceddecb9 --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/DCT/PublicHeaders/DCT/YuvConversion.h @@ -0,0 +1,9 @@ +#ifndef YuvConversion_h +#define YuvConversion_h + +#import + +void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU, uint8_t *outV, uint8_t *outA, int width, int height, int bytesPerRow); +void combineYUVAPlanesIntoARBB(uint8_t *argb, uint8_t const *inY, uint8_t const *inU, uint8_t const *inV, uint8_t const *inA, int width, int height, int bytesPerRow); + +#endif /* YuvConversion_h */ diff --git a/submodules/TelegramUI/Components/AnimationCache/DCT/Sources/DCT.m b/submodules/TelegramUI/Components/AnimationCache/DCT/Sources/DCT.m new file mode 100644 index 0000000000..b01c769007 --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/DCT/Sources/DCT.m @@ -0,0 +1,991 @@ +#import + +typedef long JLONG; + +typedef unsigned char JSAMPLE; +#define GETJSAMPLE(value) ((int)(value)) + +#define MAXJSAMPLE 255 +#define CENTERJSAMPLE 128 + +typedef short JCOEF; + +typedef unsigned int JDIMENSION; + +#define JPEG_MAX_DIMENSION 65500L /* a tad under 64K to prevent overflows */ + +#define MULTIPLIER short /* prefer 16-bit with SIMD for parellelism */ + +typedef MULTIPLIER IFAST_MULT_TYPE; /* 16 bits is OK, use short if faster */ +#define IFAST_SCALE_BITS 2 /* fractional bits in scale factors */ + +/* Various constants determining the sizes of things. + * All of these are specified by the JPEG standard, so don't change them + * if you want to be compatible. + */ + +#define DCTSIZE 8 /* The basic DCT block is 8x8 samples */ +#define DCTSIZE2 64 /* DCTSIZE squared; # of elements in a block */ +#define NUM_QUANT_TBLS 4 /* Quantization tables are numbered 0..3 */ +#define NUM_HUFF_TBLS 4 /* Huffman tables are numbered 0..3 */ +#define NUM_ARITH_TBLS 16 /* Arith-coding tables are numbered 0..15 */ +#define MAX_COMPS_IN_SCAN 4 /* JPEG limit on # of components in one scan */ +#define MAX_SAMP_FACTOR 4 /* JPEG limit on sampling factors */ +/* Unfortunately, some bozo at Adobe saw no reason to be bound by the standard; + * the PostScript DCT filter can emit files with many more than 10 blocks/MCU. + * If you happen to run across such a file, you can up D_MAX_BLOCKS_IN_MCU + * to handle it. We even let you do this from the jconfig.h file. However, + * we strongly discourage changing C_MAX_BLOCKS_IN_MCU; just because Adobe + * sometimes emits noncompliant files doesn't mean you should too. + */ +#define C_MAX_BLOCKS_IN_MCU 10 /* compressor's limit on blocks per MCU */ +#ifndef D_MAX_BLOCKS_IN_MCU +#define D_MAX_BLOCKS_IN_MCU 10 /* decompressor's limit on blocks per MCU */ +#endif + + +/* Data structures for images (arrays of samples and of DCT coefficients). + */ + +typedef JSAMPLE *JSAMPROW; /* ptr to one image row of pixel samples. */ +typedef JSAMPROW *JSAMPARRAY; /* ptr to some rows (a 2-D sample array) */ +typedef JSAMPARRAY *JSAMPIMAGE; /* a 3-D sample array: top index is color */ + +typedef JCOEF JBLOCK[DCTSIZE2]; /* one block of coefficients */ +typedef JBLOCK *JBLOCKROW; /* pointer to one row of coefficient blocks */ +typedef JBLOCKROW *JBLOCKARRAY; /* a 2-D array of coefficient blocks */ +typedef JBLOCKARRAY *JBLOCKIMAGE; /* a 3-D array of coefficient blocks */ + +typedef JCOEF *JCOEFPTR; /* useful in a couple of places */ + +#include + +/* jsimd_idct_ifast_neon() performs dequantization and a fast, not so accurate + * inverse DCT (Discrete Cosine Transform) on one block of coefficients. It + * uses the same calculations and produces exactly the same output as IJG's + * original jpeg_idct_ifast() function, which can be found in jidctfst.c. + * + * Scaled integer constants are used to avoid floating-point arithmetic: + * 0.082392200 = 2688 * 2^-15 + * 0.414213562 = 13568 * 2^-15 + * 0.847759065 = 27776 * 2^-15 + * 0.613125930 = 20096 * 2^-15 + * + * See jidctfst.c for further details of the IDCT algorithm. Where possible, + * the variable names and comments here in jsimd_idct_ifast_neon() match up + * with those in jpeg_idct_ifast(). + */ + +#define PASS1_BITS 2 + +#define F_0_082 2688 +#define F_0_414 13568 +#define F_0_847 27776 +#define F_0_613 20096 + + +__attribute__((aligned(16))) static const int16_t jsimd_idct_ifast_neon_consts[] = { + F_0_082, F_0_414, F_0_847, F_0_613 +}; + +#define F_0_382 12544 +#define F_0_541 17792 +#define F_0_707 23168 +#define F_0_306 9984 + + +__attribute__((aligned(16))) static const int16_t jsimd_fdct_ifast_neon_consts[] = { + F_0_382, F_0_541, F_0_707, F_0_306 +}; + +typedef short DCTELEM; /* prefer 16 bit with SIMD for parellelism */ +typedef unsigned short UDCTELEM; +typedef unsigned int UDCTELEM2; + +static void jsimd_fdct_ifast_neon(DCTELEM *data) { + /* Load an 8x8 block of samples into Neon registers. De-interleaving loads + * are used, followed by vuzp to transpose the block such that we have a + * column of samples per vector - allowing all rows to be processed at once. + */ + int16x8x4_t data1 = vld4q_s16(data); + int16x8x4_t data2 = vld4q_s16(data + 4 * DCTSIZE); + + int16x8x2_t cols_04 = vuzpq_s16(data1.val[0], data2.val[0]); + int16x8x2_t cols_15 = vuzpq_s16(data1.val[1], data2.val[1]); + int16x8x2_t cols_26 = vuzpq_s16(data1.val[2], data2.val[2]); + int16x8x2_t cols_37 = vuzpq_s16(data1.val[3], data2.val[3]); + + int16x8_t col0 = cols_04.val[0]; + int16x8_t col1 = cols_15.val[0]; + int16x8_t col2 = cols_26.val[0]; + int16x8_t col3 = cols_37.val[0]; + int16x8_t col4 = cols_04.val[1]; + int16x8_t col5 = cols_15.val[1]; + int16x8_t col6 = cols_26.val[1]; + int16x8_t col7 = cols_37.val[1]; + + /* Pass 1: process rows. */ + + /* Load DCT conversion constants. */ + const int16x4_t consts = vld1_s16(jsimd_fdct_ifast_neon_consts); + + int16x8_t tmp0 = vaddq_s16(col0, col7); + int16x8_t tmp7 = vsubq_s16(col0, col7); + int16x8_t tmp1 = vaddq_s16(col1, col6); + int16x8_t tmp6 = vsubq_s16(col1, col6); + int16x8_t tmp2 = vaddq_s16(col2, col5); + int16x8_t tmp5 = vsubq_s16(col2, col5); + int16x8_t tmp3 = vaddq_s16(col3, col4); + int16x8_t tmp4 = vsubq_s16(col3, col4); + + /* Even part */ + int16x8_t tmp10 = vaddq_s16(tmp0, tmp3); /* phase 2 */ + int16x8_t tmp13 = vsubq_s16(tmp0, tmp3); + int16x8_t tmp11 = vaddq_s16(tmp1, tmp2); + int16x8_t tmp12 = vsubq_s16(tmp1, tmp2); + + col0 = vaddq_s16(tmp10, tmp11); /* phase 3 */ + col4 = vsubq_s16(tmp10, tmp11); + + int16x8_t z1 = vqdmulhq_lane_s16(vaddq_s16(tmp12, tmp13), consts, 2); + col2 = vaddq_s16(tmp13, z1); /* phase 5 */ + col6 = vsubq_s16(tmp13, z1); + + /* Odd part */ + tmp10 = vaddq_s16(tmp4, tmp5); /* phase 2 */ + tmp11 = vaddq_s16(tmp5, tmp6); + tmp12 = vaddq_s16(tmp6, tmp7); + + int16x8_t z5 = vqdmulhq_lane_s16(vsubq_s16(tmp10, tmp12), consts, 0); + int16x8_t z2 = vqdmulhq_lane_s16(tmp10, consts, 1); + z2 = vaddq_s16(z2, z5); + int16x8_t z4 = vqdmulhq_lane_s16(tmp12, consts, 3); + z5 = vaddq_s16(tmp12, z5); + z4 = vaddq_s16(z4, z5); + int16x8_t z3 = vqdmulhq_lane_s16(tmp11, consts, 2); + + int16x8_t z11 = vaddq_s16(tmp7, z3); /* phase 5 */ + int16x8_t z13 = vsubq_s16(tmp7, z3); + + col5 = vaddq_s16(z13, z2); /* phase 6 */ + col3 = vsubq_s16(z13, z2); + col1 = vaddq_s16(z11, z4); + col7 = vsubq_s16(z11, z4); + + /* Transpose to work on columns in pass 2. */ + int16x8x2_t cols_01 = vtrnq_s16(col0, col1); + int16x8x2_t cols_23 = vtrnq_s16(col2, col3); + int16x8x2_t cols_45 = vtrnq_s16(col4, col5); + int16x8x2_t cols_67 = vtrnq_s16(col6, col7); + + int32x4x2_t cols_0145_l = vtrnq_s32(vreinterpretq_s32_s16(cols_01.val[0]), + vreinterpretq_s32_s16(cols_45.val[0])); + int32x4x2_t cols_0145_h = vtrnq_s32(vreinterpretq_s32_s16(cols_01.val[1]), + vreinterpretq_s32_s16(cols_45.val[1])); + int32x4x2_t cols_2367_l = vtrnq_s32(vreinterpretq_s32_s16(cols_23.val[0]), + vreinterpretq_s32_s16(cols_67.val[0])); + int32x4x2_t cols_2367_h = vtrnq_s32(vreinterpretq_s32_s16(cols_23.val[1]), + vreinterpretq_s32_s16(cols_67.val[1])); + + int32x4x2_t rows_04 = vzipq_s32(cols_0145_l.val[0], cols_2367_l.val[0]); + int32x4x2_t rows_15 = vzipq_s32(cols_0145_h.val[0], cols_2367_h.val[0]); + int32x4x2_t rows_26 = vzipq_s32(cols_0145_l.val[1], cols_2367_l.val[1]); + int32x4x2_t rows_37 = vzipq_s32(cols_0145_h.val[1], cols_2367_h.val[1]); + + int16x8_t row0 = vreinterpretq_s16_s32(rows_04.val[0]); + int16x8_t row1 = vreinterpretq_s16_s32(rows_15.val[0]); + int16x8_t row2 = vreinterpretq_s16_s32(rows_26.val[0]); + int16x8_t row3 = vreinterpretq_s16_s32(rows_37.val[0]); + int16x8_t row4 = vreinterpretq_s16_s32(rows_04.val[1]); + int16x8_t row5 = vreinterpretq_s16_s32(rows_15.val[1]); + int16x8_t row6 = vreinterpretq_s16_s32(rows_26.val[1]); + int16x8_t row7 = vreinterpretq_s16_s32(rows_37.val[1]); + + /* Pass 2: process columns. */ + + tmp0 = vaddq_s16(row0, row7); + tmp7 = vsubq_s16(row0, row7); + tmp1 = vaddq_s16(row1, row6); + tmp6 = vsubq_s16(row1, row6); + tmp2 = vaddq_s16(row2, row5); + tmp5 = vsubq_s16(row2, row5); + tmp3 = vaddq_s16(row3, row4); + tmp4 = vsubq_s16(row3, row4); + + /* Even part */ + tmp10 = vaddq_s16(tmp0, tmp3); /* phase 2 */ + tmp13 = vsubq_s16(tmp0, tmp3); + tmp11 = vaddq_s16(tmp1, tmp2); + tmp12 = vsubq_s16(tmp1, tmp2); + + row0 = vaddq_s16(tmp10, tmp11); /* phase 3 */ + row4 = vsubq_s16(tmp10, tmp11); + + z1 = vqdmulhq_lane_s16(vaddq_s16(tmp12, tmp13), consts, 2); + row2 = vaddq_s16(tmp13, z1); /* phase 5 */ + row6 = vsubq_s16(tmp13, z1); + + /* Odd part */ + tmp10 = vaddq_s16(tmp4, tmp5); /* phase 2 */ + tmp11 = vaddq_s16(tmp5, tmp6); + tmp12 = vaddq_s16(tmp6, tmp7); + + z5 = vqdmulhq_lane_s16(vsubq_s16(tmp10, tmp12), consts, 0); + z2 = vqdmulhq_lane_s16(tmp10, consts, 1); + z2 = vaddq_s16(z2, z5); + z4 = vqdmulhq_lane_s16(tmp12, consts, 3); + z5 = vaddq_s16(tmp12, z5); + z4 = vaddq_s16(z4, z5); + z3 = vqdmulhq_lane_s16(tmp11, consts, 2); + + z11 = vaddq_s16(tmp7, z3); /* phase 5 */ + z13 = vsubq_s16(tmp7, z3); + + row5 = vaddq_s16(z13, z2); /* phase 6 */ + row3 = vsubq_s16(z13, z2); + row1 = vaddq_s16(z11, z4); + row7 = vsubq_s16(z11, z4); + + vst1q_s16(data + 0 * DCTSIZE, row0); + vst1q_s16(data + 1 * DCTSIZE, row1); + vst1q_s16(data + 2 * DCTSIZE, row2); + vst1q_s16(data + 3 * DCTSIZE, row3); + vst1q_s16(data + 4 * DCTSIZE, row4); + vst1q_s16(data + 5 * DCTSIZE, row5); + vst1q_s16(data + 6 * DCTSIZE, row6); + vst1q_s16(data + 7 * DCTSIZE, row7); +} + +static void jsimd_idct_ifast_neon(void *dct_table, JCOEFPTR coef_block, + JSAMPROW output_buf) +{ + IFAST_MULT_TYPE *quantptr = dct_table; + + /* Load DCT coefficients. */ + int16x8_t row0 = vld1q_s16(coef_block + 0 * DCTSIZE); + int16x8_t row1 = vld1q_s16(coef_block + 1 * DCTSIZE); + int16x8_t row2 = vld1q_s16(coef_block + 2 * DCTSIZE); + int16x8_t row3 = vld1q_s16(coef_block + 3 * DCTSIZE); + int16x8_t row4 = vld1q_s16(coef_block + 4 * DCTSIZE); + int16x8_t row5 = vld1q_s16(coef_block + 5 * DCTSIZE); + int16x8_t row6 = vld1q_s16(coef_block + 6 * DCTSIZE); + int16x8_t row7 = vld1q_s16(coef_block + 7 * DCTSIZE); + + /* Load quantization table values for DC coefficients. */ + int16x8_t quant_row0 = vld1q_s16(quantptr + 0 * DCTSIZE); + /* Dequantize DC coefficients. */ + row0 = vmulq_s16(row0, quant_row0); + + /* Construct bitmap to test if all AC coefficients are 0. */ + int16x8_t bitmap = vorrq_s16(row1, row2); + bitmap = vorrq_s16(bitmap, row3); + bitmap = vorrq_s16(bitmap, row4); + bitmap = vorrq_s16(bitmap, row5); + bitmap = vorrq_s16(bitmap, row6); + bitmap = vorrq_s16(bitmap, row7); + + int64_t left_ac_bitmap = vgetq_lane_s64(vreinterpretq_s64_s16(bitmap), 0); + int64_t right_ac_bitmap = vgetq_lane_s64(vreinterpretq_s64_s16(bitmap), 1); + + /* Load IDCT conversion constants. */ + const int16x4_t consts = vld1_s16(jsimd_idct_ifast_neon_consts); + + if (left_ac_bitmap == 0 && right_ac_bitmap == 0) { + /* All AC coefficients are zero. + * Compute DC values and duplicate into vectors. + */ + int16x8_t dcval = row0; + row1 = dcval; + row2 = dcval; + row3 = dcval; + row4 = dcval; + row5 = dcval; + row6 = dcval; + row7 = dcval; + } else if (left_ac_bitmap == 0) { + /* AC coefficients are zero for columns 0, 1, 2, and 3. + * Use DC values for these columns. + */ + int16x4_t dcval = vget_low_s16(row0); + + /* Commence regular fast IDCT computation for columns 4, 5, 6, and 7. */ + + /* Load quantization table. */ + int16x4_t quant_row1 = vld1_s16(quantptr + 1 * DCTSIZE + 4); + int16x4_t quant_row2 = vld1_s16(quantptr + 2 * DCTSIZE + 4); + int16x4_t quant_row3 = vld1_s16(quantptr + 3 * DCTSIZE + 4); + int16x4_t quant_row4 = vld1_s16(quantptr + 4 * DCTSIZE + 4); + int16x4_t quant_row5 = vld1_s16(quantptr + 5 * DCTSIZE + 4); + int16x4_t quant_row6 = vld1_s16(quantptr + 6 * DCTSIZE + 4); + int16x4_t quant_row7 = vld1_s16(quantptr + 7 * DCTSIZE + 4); + + /* Even part: dequantize DCT coefficients. */ + int16x4_t tmp0 = vget_high_s16(row0); + int16x4_t tmp1 = vmul_s16(vget_high_s16(row2), quant_row2); + int16x4_t tmp2 = vmul_s16(vget_high_s16(row4), quant_row4); + int16x4_t tmp3 = vmul_s16(vget_high_s16(row6), quant_row6); + + int16x4_t tmp10 = vadd_s16(tmp0, tmp2); /* phase 3 */ + int16x4_t tmp11 = vsub_s16(tmp0, tmp2); + + int16x4_t tmp13 = vadd_s16(tmp1, tmp3); /* phases 5-3 */ + int16x4_t tmp1_sub_tmp3 = vsub_s16(tmp1, tmp3); + int16x4_t tmp12 = vqdmulh_lane_s16(tmp1_sub_tmp3, consts, 1); + tmp12 = vadd_s16(tmp12, tmp1_sub_tmp3); + tmp12 = vsub_s16(tmp12, tmp13); + + tmp0 = vadd_s16(tmp10, tmp13); /* phase 2 */ + tmp3 = vsub_s16(tmp10, tmp13); + tmp1 = vadd_s16(tmp11, tmp12); + tmp2 = vsub_s16(tmp11, tmp12); + + /* Odd part: dequantize DCT coefficients. */ + int16x4_t tmp4 = vmul_s16(vget_high_s16(row1), quant_row1); + int16x4_t tmp5 = vmul_s16(vget_high_s16(row3), quant_row3); + int16x4_t tmp6 = vmul_s16(vget_high_s16(row5), quant_row5); + int16x4_t tmp7 = vmul_s16(vget_high_s16(row7), quant_row7); + + int16x4_t z13 = vadd_s16(tmp6, tmp5); /* phase 6 */ + int16x4_t neg_z10 = vsub_s16(tmp5, tmp6); + int16x4_t z11 = vadd_s16(tmp4, tmp7); + int16x4_t z12 = vsub_s16(tmp4, tmp7); + + tmp7 = vadd_s16(z11, z13); /* phase 5 */ + int16x4_t z11_sub_z13 = vsub_s16(z11, z13); + tmp11 = vqdmulh_lane_s16(z11_sub_z13, consts, 1); + tmp11 = vadd_s16(tmp11, z11_sub_z13); + + int16x4_t z10_add_z12 = vsub_s16(z12, neg_z10); + int16x4_t z5 = vqdmulh_lane_s16(z10_add_z12, consts, 2); + z5 = vadd_s16(z5, z10_add_z12); + tmp10 = vqdmulh_lane_s16(z12, consts, 0); + tmp10 = vadd_s16(tmp10, z12); + tmp10 = vsub_s16(tmp10, z5); + tmp12 = vqdmulh_lane_s16(neg_z10, consts, 3); + tmp12 = vadd_s16(tmp12, vadd_s16(neg_z10, neg_z10)); + tmp12 = vadd_s16(tmp12, z5); + + tmp6 = vsub_s16(tmp12, tmp7); /* phase 2 */ + tmp5 = vsub_s16(tmp11, tmp6); + tmp4 = vadd_s16(tmp10, tmp5); + + row0 = vcombine_s16(dcval, vadd_s16(tmp0, tmp7)); + row7 = vcombine_s16(dcval, vsub_s16(tmp0, tmp7)); + row1 = vcombine_s16(dcval, vadd_s16(tmp1, tmp6)); + row6 = vcombine_s16(dcval, vsub_s16(tmp1, tmp6)); + row2 = vcombine_s16(dcval, vadd_s16(tmp2, tmp5)); + row5 = vcombine_s16(dcval, vsub_s16(tmp2, tmp5)); + row4 = vcombine_s16(dcval, vadd_s16(tmp3, tmp4)); + row3 = vcombine_s16(dcval, vsub_s16(tmp3, tmp4)); + } else if (right_ac_bitmap == 0) { + /* AC coefficients are zero for columns 4, 5, 6, and 7. + * Use DC values for these columns. + */ + int16x4_t dcval = vget_high_s16(row0); + + /* Commence regular fast IDCT computation for columns 0, 1, 2, and 3. */ + + /* Load quantization table. */ + int16x4_t quant_row1 = vld1_s16(quantptr + 1 * DCTSIZE); + int16x4_t quant_row2 = vld1_s16(quantptr + 2 * DCTSIZE); + int16x4_t quant_row3 = vld1_s16(quantptr + 3 * DCTSIZE); + int16x4_t quant_row4 = vld1_s16(quantptr + 4 * DCTSIZE); + int16x4_t quant_row5 = vld1_s16(quantptr + 5 * DCTSIZE); + int16x4_t quant_row6 = vld1_s16(quantptr + 6 * DCTSIZE); + int16x4_t quant_row7 = vld1_s16(quantptr + 7 * DCTSIZE); + + /* Even part: dequantize DCT coefficients. */ + int16x4_t tmp0 = vget_low_s16(row0); + int16x4_t tmp1 = vmul_s16(vget_low_s16(row2), quant_row2); + int16x4_t tmp2 = vmul_s16(vget_low_s16(row4), quant_row4); + int16x4_t tmp3 = vmul_s16(vget_low_s16(row6), quant_row6); + + int16x4_t tmp10 = vadd_s16(tmp0, tmp2); /* phase 3 */ + int16x4_t tmp11 = vsub_s16(tmp0, tmp2); + + int16x4_t tmp13 = vadd_s16(tmp1, tmp3); /* phases 5-3 */ + int16x4_t tmp1_sub_tmp3 = vsub_s16(tmp1, tmp3); + int16x4_t tmp12 = vqdmulh_lane_s16(tmp1_sub_tmp3, consts, 1); + tmp12 = vadd_s16(tmp12, tmp1_sub_tmp3); + tmp12 = vsub_s16(tmp12, tmp13); + + tmp0 = vadd_s16(tmp10, tmp13); /* phase 2 */ + tmp3 = vsub_s16(tmp10, tmp13); + tmp1 = vadd_s16(tmp11, tmp12); + tmp2 = vsub_s16(tmp11, tmp12); + + /* Odd part: dequantize DCT coefficients. */ + int16x4_t tmp4 = vmul_s16(vget_low_s16(row1), quant_row1); + int16x4_t tmp5 = vmul_s16(vget_low_s16(row3), quant_row3); + int16x4_t tmp6 = vmul_s16(vget_low_s16(row5), quant_row5); + int16x4_t tmp7 = vmul_s16(vget_low_s16(row7), quant_row7); + + int16x4_t z13 = vadd_s16(tmp6, tmp5); /* phase 6 */ + int16x4_t neg_z10 = vsub_s16(tmp5, tmp6); + int16x4_t z11 = vadd_s16(tmp4, tmp7); + int16x4_t z12 = vsub_s16(tmp4, tmp7); + + tmp7 = vadd_s16(z11, z13); /* phase 5 */ + int16x4_t z11_sub_z13 = vsub_s16(z11, z13); + tmp11 = vqdmulh_lane_s16(z11_sub_z13, consts, 1); + tmp11 = vadd_s16(tmp11, z11_sub_z13); + + int16x4_t z10_add_z12 = vsub_s16(z12, neg_z10); + int16x4_t z5 = vqdmulh_lane_s16(z10_add_z12, consts, 2); + z5 = vadd_s16(z5, z10_add_z12); + tmp10 = vqdmulh_lane_s16(z12, consts, 0); + tmp10 = vadd_s16(tmp10, z12); + tmp10 = vsub_s16(tmp10, z5); + tmp12 = vqdmulh_lane_s16(neg_z10, consts, 3); + tmp12 = vadd_s16(tmp12, vadd_s16(neg_z10, neg_z10)); + tmp12 = vadd_s16(tmp12, z5); + + tmp6 = vsub_s16(tmp12, tmp7); /* phase 2 */ + tmp5 = vsub_s16(tmp11, tmp6); + tmp4 = vadd_s16(tmp10, tmp5); + + row0 = vcombine_s16(vadd_s16(tmp0, tmp7), dcval); + row7 = vcombine_s16(vsub_s16(tmp0, tmp7), dcval); + row1 = vcombine_s16(vadd_s16(tmp1, tmp6), dcval); + row6 = vcombine_s16(vsub_s16(tmp1, tmp6), dcval); + row2 = vcombine_s16(vadd_s16(tmp2, tmp5), dcval); + row5 = vcombine_s16(vsub_s16(tmp2, tmp5), dcval); + row4 = vcombine_s16(vadd_s16(tmp3, tmp4), dcval); + row3 = vcombine_s16(vsub_s16(tmp3, tmp4), dcval); + } else { + /* Some AC coefficients are non-zero; full IDCT calculation required. */ + + /* Load quantization table. */ + int16x8_t quant_row1 = vld1q_s16(quantptr + 1 * DCTSIZE); + int16x8_t quant_row2 = vld1q_s16(quantptr + 2 * DCTSIZE); + int16x8_t quant_row3 = vld1q_s16(quantptr + 3 * DCTSIZE); + int16x8_t quant_row4 = vld1q_s16(quantptr + 4 * DCTSIZE); + int16x8_t quant_row5 = vld1q_s16(quantptr + 5 * DCTSIZE); + int16x8_t quant_row6 = vld1q_s16(quantptr + 6 * DCTSIZE); + int16x8_t quant_row7 = vld1q_s16(quantptr + 7 * DCTSIZE); + + /* Even part: dequantize DCT coefficients. */ + int16x8_t tmp0 = row0; + int16x8_t tmp1 = vmulq_s16(row2, quant_row2); + int16x8_t tmp2 = vmulq_s16(row4, quant_row4); + int16x8_t tmp3 = vmulq_s16(row6, quant_row6); + + int16x8_t tmp10 = vaddq_s16(tmp0, tmp2); /* phase 3 */ + int16x8_t tmp11 = vsubq_s16(tmp0, tmp2); + + int16x8_t tmp13 = vaddq_s16(tmp1, tmp3); /* phases 5-3 */ + int16x8_t tmp1_sub_tmp3 = vsubq_s16(tmp1, tmp3); + int16x8_t tmp12 = vqdmulhq_lane_s16(tmp1_sub_tmp3, consts, 1); + tmp12 = vaddq_s16(tmp12, tmp1_sub_tmp3); + tmp12 = vsubq_s16(tmp12, tmp13); + + tmp0 = vaddq_s16(tmp10, tmp13); /* phase 2 */ + tmp3 = vsubq_s16(tmp10, tmp13); + tmp1 = vaddq_s16(tmp11, tmp12); + tmp2 = vsubq_s16(tmp11, tmp12); + + /* Odd part: dequantize DCT coefficients. */ + int16x8_t tmp4 = vmulq_s16(row1, quant_row1); + int16x8_t tmp5 = vmulq_s16(row3, quant_row3); + int16x8_t tmp6 = vmulq_s16(row5, quant_row5); + int16x8_t tmp7 = vmulq_s16(row7, quant_row7); + + int16x8_t z13 = vaddq_s16(tmp6, tmp5); /* phase 6 */ + int16x8_t neg_z10 = vsubq_s16(tmp5, tmp6); + int16x8_t z11 = vaddq_s16(tmp4, tmp7); + int16x8_t z12 = vsubq_s16(tmp4, tmp7); + + tmp7 = vaddq_s16(z11, z13); /* phase 5 */ + int16x8_t z11_sub_z13 = vsubq_s16(z11, z13); + tmp11 = vqdmulhq_lane_s16(z11_sub_z13, consts, 1); + tmp11 = vaddq_s16(tmp11, z11_sub_z13); + + int16x8_t z10_add_z12 = vsubq_s16(z12, neg_z10); + int16x8_t z5 = vqdmulhq_lane_s16(z10_add_z12, consts, 2); + z5 = vaddq_s16(z5, z10_add_z12); + tmp10 = vqdmulhq_lane_s16(z12, consts, 0); + tmp10 = vaddq_s16(tmp10, z12); + tmp10 = vsubq_s16(tmp10, z5); + tmp12 = vqdmulhq_lane_s16(neg_z10, consts, 3); + tmp12 = vaddq_s16(tmp12, vaddq_s16(neg_z10, neg_z10)); + tmp12 = vaddq_s16(tmp12, z5); + + tmp6 = vsubq_s16(tmp12, tmp7); /* phase 2 */ + tmp5 = vsubq_s16(tmp11, tmp6); + tmp4 = vaddq_s16(tmp10, tmp5); + + row0 = vaddq_s16(tmp0, tmp7); + row7 = vsubq_s16(tmp0, tmp7); + row1 = vaddq_s16(tmp1, tmp6); + row6 = vsubq_s16(tmp1, tmp6); + row2 = vaddq_s16(tmp2, tmp5); + row5 = vsubq_s16(tmp2, tmp5); + row4 = vaddq_s16(tmp3, tmp4); + row3 = vsubq_s16(tmp3, tmp4); + } + + /* Transpose rows to work on columns in pass 2. */ + int16x8x2_t rows_01 = vtrnq_s16(row0, row1); + int16x8x2_t rows_23 = vtrnq_s16(row2, row3); + int16x8x2_t rows_45 = vtrnq_s16(row4, row5); + int16x8x2_t rows_67 = vtrnq_s16(row6, row7); + + int32x4x2_t rows_0145_l = vtrnq_s32(vreinterpretq_s32_s16(rows_01.val[0]), + vreinterpretq_s32_s16(rows_45.val[0])); + int32x4x2_t rows_0145_h = vtrnq_s32(vreinterpretq_s32_s16(rows_01.val[1]), + vreinterpretq_s32_s16(rows_45.val[1])); + int32x4x2_t rows_2367_l = vtrnq_s32(vreinterpretq_s32_s16(rows_23.val[0]), + vreinterpretq_s32_s16(rows_67.val[0])); + int32x4x2_t rows_2367_h = vtrnq_s32(vreinterpretq_s32_s16(rows_23.val[1]), + vreinterpretq_s32_s16(rows_67.val[1])); + + int32x4x2_t cols_04 = vzipq_s32(rows_0145_l.val[0], rows_2367_l.val[0]); + int32x4x2_t cols_15 = vzipq_s32(rows_0145_h.val[0], rows_2367_h.val[0]); + int32x4x2_t cols_26 = vzipq_s32(rows_0145_l.val[1], rows_2367_l.val[1]); + int32x4x2_t cols_37 = vzipq_s32(rows_0145_h.val[1], rows_2367_h.val[1]); + + int16x8_t col0 = vreinterpretq_s16_s32(cols_04.val[0]); + int16x8_t col1 = vreinterpretq_s16_s32(cols_15.val[0]); + int16x8_t col2 = vreinterpretq_s16_s32(cols_26.val[0]); + int16x8_t col3 = vreinterpretq_s16_s32(cols_37.val[0]); + int16x8_t col4 = vreinterpretq_s16_s32(cols_04.val[1]); + int16x8_t col5 = vreinterpretq_s16_s32(cols_15.val[1]); + int16x8_t col6 = vreinterpretq_s16_s32(cols_26.val[1]); + int16x8_t col7 = vreinterpretq_s16_s32(cols_37.val[1]); + + /* 1-D IDCT, pass 2 */ + + /* Even part */ + int16x8_t tmp10 = vaddq_s16(col0, col4); + int16x8_t tmp11 = vsubq_s16(col0, col4); + + int16x8_t tmp13 = vaddq_s16(col2, col6); + int16x8_t col2_sub_col6 = vsubq_s16(col2, col6); + int16x8_t tmp12 = vqdmulhq_lane_s16(col2_sub_col6, consts, 1); + tmp12 = vaddq_s16(tmp12, col2_sub_col6); + tmp12 = vsubq_s16(tmp12, tmp13); + + int16x8_t tmp0 = vaddq_s16(tmp10, tmp13); + int16x8_t tmp3 = vsubq_s16(tmp10, tmp13); + int16x8_t tmp1 = vaddq_s16(tmp11, tmp12); + int16x8_t tmp2 = vsubq_s16(tmp11, tmp12); + + /* Odd part */ + int16x8_t z13 = vaddq_s16(col5, col3); + int16x8_t neg_z10 = vsubq_s16(col3, col5); + int16x8_t z11 = vaddq_s16(col1, col7); + int16x8_t z12 = vsubq_s16(col1, col7); + + int16x8_t tmp7 = vaddq_s16(z11, z13); /* phase 5 */ + int16x8_t z11_sub_z13 = vsubq_s16(z11, z13); + tmp11 = vqdmulhq_lane_s16(z11_sub_z13, consts, 1); + tmp11 = vaddq_s16(tmp11, z11_sub_z13); + + int16x8_t z10_add_z12 = vsubq_s16(z12, neg_z10); + int16x8_t z5 = vqdmulhq_lane_s16(z10_add_z12, consts, 2); + z5 = vaddq_s16(z5, z10_add_z12); + tmp10 = vqdmulhq_lane_s16(z12, consts, 0); + tmp10 = vaddq_s16(tmp10, z12); + tmp10 = vsubq_s16(tmp10, z5); + tmp12 = vqdmulhq_lane_s16(neg_z10, consts, 3); + tmp12 = vaddq_s16(tmp12, vaddq_s16(neg_z10, neg_z10)); + tmp12 = vaddq_s16(tmp12, z5); + + int16x8_t tmp6 = vsubq_s16(tmp12, tmp7); /* phase 2 */ + int16x8_t tmp5 = vsubq_s16(tmp11, tmp6); + int16x8_t tmp4 = vaddq_s16(tmp10, tmp5); + + col0 = vaddq_s16(tmp0, tmp7); + col7 = vsubq_s16(tmp0, tmp7); + col1 = vaddq_s16(tmp1, tmp6); + col6 = vsubq_s16(tmp1, tmp6); + col2 = vaddq_s16(tmp2, tmp5); + col5 = vsubq_s16(tmp2, tmp5); + col4 = vaddq_s16(tmp3, tmp4); + col3 = vsubq_s16(tmp3, tmp4); + + /* Scale down by a factor of 8, narrowing to 8-bit. */ + int8x16_t cols_01_s8 = vcombine_s8(vqshrn_n_s16(col0, PASS1_BITS + 3), + vqshrn_n_s16(col1, PASS1_BITS + 3)); + int8x16_t cols_45_s8 = vcombine_s8(vqshrn_n_s16(col4, PASS1_BITS + 3), + vqshrn_n_s16(col5, PASS1_BITS + 3)); + int8x16_t cols_23_s8 = vcombine_s8(vqshrn_n_s16(col2, PASS1_BITS + 3), + vqshrn_n_s16(col3, PASS1_BITS + 3)); + int8x16_t cols_67_s8 = vcombine_s8(vqshrn_n_s16(col6, PASS1_BITS + 3), + vqshrn_n_s16(col7, PASS1_BITS + 3)); + /* Clamp to range [0-255]. */ + uint8x16_t cols_01 = + vreinterpretq_u8_s8 + (vaddq_s8(cols_01_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE)))); + uint8x16_t cols_45 = + vreinterpretq_u8_s8 + (vaddq_s8(cols_45_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE)))); + uint8x16_t cols_23 = + vreinterpretq_u8_s8 + (vaddq_s8(cols_23_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE)))); + uint8x16_t cols_67 = + vreinterpretq_u8_s8 + (vaddq_s8(cols_67_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE)))); + + /* Transpose block to prepare for store. */ + uint32x4x2_t cols_0415 = vzipq_u32(vreinterpretq_u32_u8(cols_01), + vreinterpretq_u32_u8(cols_45)); + uint32x4x2_t cols_2637 = vzipq_u32(vreinterpretq_u32_u8(cols_23), + vreinterpretq_u32_u8(cols_67)); + + uint8x16x2_t cols_0145 = vtrnq_u8(vreinterpretq_u8_u32(cols_0415.val[0]), + vreinterpretq_u8_u32(cols_0415.val[1])); + uint8x16x2_t cols_2367 = vtrnq_u8(vreinterpretq_u8_u32(cols_2637.val[0]), + vreinterpretq_u8_u32(cols_2637.val[1])); + uint16x8x2_t rows_0426 = vtrnq_u16(vreinterpretq_u16_u8(cols_0145.val[0]), + vreinterpretq_u16_u8(cols_2367.val[0])); + uint16x8x2_t rows_1537 = vtrnq_u16(vreinterpretq_u16_u8(cols_0145.val[1]), + vreinterpretq_u16_u8(cols_2367.val[1])); + + uint8x16_t rows_04 = vreinterpretq_u8_u16(rows_0426.val[0]); + uint8x16_t rows_15 = vreinterpretq_u8_u16(rows_1537.val[0]); + uint8x16_t rows_26 = vreinterpretq_u8_u16(rows_0426.val[1]); + uint8x16_t rows_37 = vreinterpretq_u8_u16(rows_1537.val[1]); + + JSAMPROW outptr0 = output_buf + DCTSIZE * 0; + JSAMPROW outptr1 = output_buf + DCTSIZE * 1; + JSAMPROW outptr2 = output_buf + DCTSIZE * 2; + JSAMPROW outptr3 = output_buf + DCTSIZE * 3; + JSAMPROW outptr4 = output_buf + DCTSIZE * 4; + JSAMPROW outptr5 = output_buf + DCTSIZE * 5; + JSAMPROW outptr6 = output_buf + DCTSIZE * 6; + JSAMPROW outptr7 = output_buf + DCTSIZE * 7; + + /* Store DCT block to memory. */ + vst1q_lane_u64((uint64_t *)outptr0, vreinterpretq_u64_u8(rows_04), 0); + vst1q_lane_u64((uint64_t *)outptr1, vreinterpretq_u64_u8(rows_15), 0); + vst1q_lane_u64((uint64_t *)outptr2, vreinterpretq_u64_u8(rows_26), 0); + vst1q_lane_u64((uint64_t *)outptr3, vreinterpretq_u64_u8(rows_37), 0); + vst1q_lane_u64((uint64_t *)outptr4, vreinterpretq_u64_u8(rows_04), 1); + vst1q_lane_u64((uint64_t *)outptr5, vreinterpretq_u64_u8(rows_15), 1); + vst1q_lane_u64((uint64_t *)outptr6, vreinterpretq_u64_u8(rows_26), 1); + vst1q_lane_u64((uint64_t *)outptr7, vreinterpretq_u64_u8(rows_37), 1); +} + +static int flss(uint16_t val) { + int bit; + + bit = 16; + + if (!val) + return 0; + + if (!(val & 0xff00)) { + bit -= 8; + val <<= 8; + } + if (!(val & 0xf000)) { + bit -= 4; + val <<= 4; + } + if (!(val & 0xc000)) { + bit -= 2; + val <<= 2; + } + if (!(val & 0x8000)) { + bit -= 1; + val <<= 1; + } + + return bit; +} + +static int compute_reciprocal(uint16_t divisor, DCTELEM *dtbl) { + UDCTELEM2 fq, fr; + UDCTELEM c; + int b, r; + + if (divisor == 1) { + /* divisor == 1 means unquantized, so these reciprocal/correction/shift + * values will cause the C quantization algorithm to act like the + * identity function. Since only the C quantization algorithm is used in + * these cases, the scale value is irrelevant. + */ + dtbl[DCTSIZE2 * 0] = (DCTELEM)1; /* reciprocal */ + dtbl[DCTSIZE2 * 1] = (DCTELEM)0; /* correction */ + dtbl[DCTSIZE2 * 2] = (DCTELEM)1; /* scale */ + dtbl[DCTSIZE2 * 3] = -(DCTELEM)(sizeof(DCTELEM) * 8); /* shift */ + return 0; + } + + b = flss(divisor) - 1; + r = sizeof(DCTELEM) * 8 + b; + + fq = ((UDCTELEM2)1 << r) / divisor; + fr = ((UDCTELEM2)1 << r) % divisor; + + c = divisor / 2; /* for rounding */ + + if (fr == 0) { /* divisor is power of two */ + /* fq will be one bit too large to fit in DCTELEM, so adjust */ + fq >>= 1; + r--; + } else if (fr <= (divisor / 2U)) { /* fractional part is < 0.5 */ + c++; + } else { /* fractional part is > 0.5 */ + fq++; + } + + dtbl[DCTSIZE2 * 0] = (DCTELEM)fq; /* reciprocal */ + dtbl[DCTSIZE2 * 1] = (DCTELEM)c; /* correction + roundfactor */ +#ifdef WITH_SIMD + dtbl[DCTSIZE2 * 2] = (DCTELEM)(1 << (sizeof(DCTELEM) * 8 * 2 - r)); /* scale */ +#else + dtbl[DCTSIZE2 * 2] = 1; +#endif + dtbl[DCTSIZE2 * 3] = (DCTELEM)r - sizeof(DCTELEM) * 8; /* shift */ + + if (r <= 16) return 0; + else return 1; +} + +#define DESCALE(x, n) RIGHT_SHIFT(x, n) + + +/* Multiply a DCTELEM variable by an JLONG constant, and immediately + * descale to yield a DCTELEM result. + */ + +#define MULTIPLY(var, const) ((DCTELEM)DESCALE((var) * (const), CONST_BITS)) +#define MULTIPLY16V16(var1, var2) ((var1) * (var2)) + +static DCTELEM std_luminance_quant_tbl[DCTSIZE2] = { + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68, 109, 103, 77, + 24, 35, 55, 64, 81, 104, 113, 92, + 49, 64, 78, 87, 103, 121, 120, 101, + 72, 92, 95, 98, 112, 100, 103, 99 +}; + +static int jpeg_quality_scaling(int quality) +/* Convert a user-specified quality rating to a percentage scaling factor + * for an underlying quantization table, using our recommended scaling curve. + * The input 'quality' factor should be 0 (terrible) to 100 (very good). + */ +{ + /* Safety limit on quality factor. Convert 0 to 1 to avoid zero divide. */ + if (quality <= 0) quality = 1; + if (quality > 100) quality = 100; + + /* The basic table is used as-is (scaling 100) for a quality of 50. + * Qualities 50..100 are converted to scaling percentage 200 - 2*Q; + * note that at Q=100 the scaling is 0, which will cause jpeg_add_quant_table + * to make all the table entries 1 (hence, minimum quantization loss). + * Qualities 1..50 are converted to scaling percentage 5000/Q. + */ + if (quality < 50) + quality = 5000 / quality; + else + quality = 200 - quality * 2; + + return quality; +} + +static void jpeg_add_quant_table(DCTELEM *qtable, DCTELEM *basicTable, int scale_factor, bool forceBaseline) +/* Define a quantization table equal to the basic_table times + * a scale factor (given as a percentage). + * If force_baseline is TRUE, the computed quantization table entries + * are limited to 1..255 for JPEG baseline compatibility. + */ +{ + int i; + long temp; + + for (i = 0; i < DCTSIZE2; i++) { + temp = ((long)basicTable[i] * scale_factor + 50L) / 100L; + /* limit the values to the valid range */ + if (temp <= 0L) temp = 1L; + if (temp > 32767L) temp = 32767L; /* max quantizer needed for 12 bits */ + if (forceBaseline && temp > 255L) + temp = 255L; /* limit to baseline range if requested */ + qtable[i] = (uint16_t)temp; + } +} + +static void jpeg_set_quality(DCTELEM *qtable, int quality) +/* Set or change the 'quality' (quantization) setting, using default tables. + * This is the standard quality-adjusting entry point for typical user + * interfaces; only those who want detailed control over quantization tables + * would use the preceding three routines directly. + */ +{ + /* Convert user 0-100 rating to percentage scaling */ + quality = jpeg_quality_scaling(quality); + + /* Set up standard quality tables */ + jpeg_add_quant_table(qtable, std_luminance_quant_tbl, quality, false); +} + +static void getDivisors(DCTELEM *dtbl, DCTELEM *qtable) { +#define CONST_BITS 14 +#define RIGHT_SHIFT(x, shft) ((x) >> (shft)) + + static const int16_t aanscales[DCTSIZE2] = { + /* precomputed values scaled up by 14 bits */ + 16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520, + 22725, 31521, 29692, 26722, 22725, 17855, 12299, 6270, + 21407, 29692, 27969, 25172, 21407, 16819, 11585, 5906, + 19266, 26722, 25172, 22654, 19266, 15137, 10426, 5315, + 16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520, + 12873, 17855, 16819, 15137, 12873, 10114, 6967, 3552, + 8867, 12299, 11585, 10426, 8867, 6967, 4799, 2446, + 4520, 6270, 5906, 5315, 4520, 3552, 2446, 1247 + }; + + for (int i = 0; i < DCTSIZE2; i++) { + if (!compute_reciprocal( + DESCALE(MULTIPLY16V16((JLONG)qtable[i], + (JLONG)aanscales[i]), + CONST_BITS - 3), &dtbl[i])) { + //fdct->quantize = quantize; + printf("here\n"); + } + } +} + +static void quantize(JCOEFPTR coef_block, DCTELEM *divisors, DCTELEM *workspace) +{ + int i; + DCTELEM temp; + JCOEFPTR output_ptr = coef_block; + + UDCTELEM recip, corr; + int shift; + UDCTELEM2 product; + + for (i = 0; i < DCTSIZE2; i++) { + temp = workspace[i]; + recip = divisors[i + DCTSIZE2 * 0]; + corr = divisors[i + DCTSIZE2 * 1]; + shift = divisors[i + DCTSIZE2 * 3]; + + if (temp < 0) { + temp = -temp; + product = (UDCTELEM2)(temp + corr) * recip; + product >>= shift + sizeof(DCTELEM) * 8; + temp = (DCTELEM)product; + temp = -temp; + } else { + product = (UDCTELEM2)(temp + corr) * recip; + product >>= shift + sizeof(DCTELEM) * 8; + temp = (DCTELEM)product; + } + output_ptr[i] = (JCOEF)temp; + } +} + +NSData *generateForwardDctData(int quality) { + NSMutableData *divisors = [[NSMutableData alloc] initWithLength:DCTSIZE2 * 4 * sizeof(DCTELEM)]; + + DCTELEM qtable[DCTSIZE2]; + jpeg_set_quality(qtable, quality); + + getDivisors((DCTELEM *)divisors.mutableBytes, qtable); + + return divisors; +} + +NSData *generateInverseDctData(int quality) { + NSMutableData *divisors = [[NSMutableData alloc] initWithLength:DCTSIZE2 * sizeof(IFAST_MULT_TYPE)]; + IFAST_MULT_TYPE *ifmtbl = (IFAST_MULT_TYPE *)divisors.mutableBytes; + + DCTELEM qtable[DCTSIZE2]; + jpeg_set_quality(qtable, quality); + +#define CONST_BITS 14 + static const int16_t aanscales[DCTSIZE2] = { + /* precomputed values scaled up by 14 bits */ + 16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520, + 22725, 31521, 29692, 26722, 22725, 17855, 12299, 6270, + 21407, 29692, 27969, 25172, 21407, 16819, 11585, 5906, + 19266, 26722, 25172, 22654, 19266, 15137, 10426, 5315, + 16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520, + 12873, 17855, 16819, 15137, 12873, 10114, 6967, 3552, + 8867, 12299, 11585, 10426, 8867, 6967, 4799, 2446, + 4520, 6270, 5906, 5315, 4520, 3552, 2446, 1247 + }; + + for (int i = 0; i < DCTSIZE2; i++) { + ifmtbl[i] = (IFAST_MULT_TYPE) + DESCALE(MULTIPLY16V16((JLONG)qtable[i], + (JLONG)aanscales[i]), + CONST_BITS - IFAST_SCALE_BITS); + } + + return divisors; +} + +static const int zigZagInv[DCTSIZE2] = { + 0,1,8,16,9,2,3,10, + 17,24,32,25,18,11,4,5, + 12,19,26,33,40,48,41,34, + 27,20,13,6,7,14,21,28, + 35,42,49,56,57,50,43,36, + 29,22,15,23,30,37,44,51, + 58,59,52,45,38,31,39,46, + 53,60,61,54,47,55,62,63 +}; + +static const int zigZag[DCTSIZE2] = { + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63 +}; + +void performForwardDct(uint8_t const *pixels, int16_t *coefficients, int width, int height, int bytesPerRow, NSData *dctData) { + DCTELEM *divisors = (DCTELEM *)dctData.bytes; + + DCTELEM block[DCTSIZE2]; + JCOEF coefBlock[DCTSIZE2]; + + for (int y = 0; y < height; y += DCTSIZE) { + for (int x = 0; x < width; x += DCTSIZE) { + for (int blockY = 0; blockY < DCTSIZE; blockY++) { + for (int blockX = 0; blockX < DCTSIZE; blockX++) { + block[blockY * DCTSIZE + blockX] = ((DCTELEM)pixels[(y + blockY) * bytesPerRow + (x + blockX)]) - CENTERJSAMPLE; + } + } + + jsimd_fdct_ifast_neon(block); + + quantize(coefBlock, divisors, block); + + for (int blockY = 0; blockY < DCTSIZE; blockY++) { + for (int blockX = 0; blockX < DCTSIZE; blockX++) { + coefficients[(y + blockY) * bytesPerRow + (x + blockX)] = coefBlock[zigZagInv[blockY * DCTSIZE + blockX]]; + } + } + } + } +} + +void performInverseDct(int16_t const *coefficients, uint8_t *pixels, int width, int height, int coefficientsPerRow, int bytesPerRow, NSData *idctData) { + IFAST_MULT_TYPE *ifmtbl = (IFAST_MULT_TYPE *)idctData.bytes; + + DCTELEM coefficientBlock[DCTSIZE2]; + JSAMPLE pixelBlock[DCTSIZE2]; + + for (int y = 0; y < height; y += DCTSIZE) { + for (int x = 0; x < width; x += DCTSIZE) { + for (int blockY = 0; blockY < DCTSIZE; blockY++) { + for (int blockX = 0; blockX < DCTSIZE; blockX++) { + coefficientBlock[zigZag[blockY * DCTSIZE + blockX]] = coefficients[(y + blockY) * coefficientsPerRow + (x + blockX)]; + } + } + + jsimd_idct_ifast_neon(ifmtbl, coefficientBlock, pixelBlock); + + for (int blockY = 0; blockY < DCTSIZE; blockY++) { + for (int blockX = 0; blockX < DCTSIZE; blockX++) { + pixels[(y + blockY) * bytesPerRow + (x + blockX)] = pixelBlock[blockY * DCTSIZE + blockX]; + } + } + } + } +} diff --git a/submodules/TelegramUI/Components/AnimationCache/DCT/Sources/YuvConversion.m b/submodules/TelegramUI/Components/AnimationCache/DCT/Sources/YuvConversion.m new file mode 100644 index 0000000000..25b244d5e1 --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/DCT/Sources/YuvConversion.m @@ -0,0 +1,99 @@ +#import + +#import +#import + +static uint8_t permuteMap[4] = { 3, 2, 1, 0}; + +void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU, uint8_t *outV, uint8_t *outA, int width, int height, int bytesPerRow) { + static vImage_ARGBToYpCbCr info; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + vImage_YpCbCrPixelRange pixelRange = (vImage_YpCbCrPixelRange){ 0, 128, 255, 255, 255, 1, 255, 0 }; + vImageConvert_ARGBToYpCbCr_GenerateConversion(kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2, &pixelRange, &info, kvImageARGB8888, kvImage420Yp8_Cb8_Cr8, 0); + }); + + vImage_Error error = kvImageNoError; + + vImage_Buffer src; + src.data = (void *)argb; + src.width = width; + src.height = height; + src.rowBytes = bytesPerRow; + + vImage_Buffer destYp; + destYp.data = outY; + destYp.width = width; + destYp.height = height; + destYp.rowBytes = width; + + vImage_Buffer destCr; + destCr.data = outU; + destCr.width = width / 2; + destCr.height = height / 2; + destCr.rowBytes = width / 2; + + vImage_Buffer destCb; + destCb.data = outV; + destCb.width = width / 2; + destCb.height = height / 2; + destCb.rowBytes = width / 2; + + vImage_Buffer destA; + destA.data = outA; + destA.width = width; + destA.height = height; + destA.rowBytes = width; + + error = vImageConvert_ARGB8888To420Yp8_Cb8_Cr8(&src, &destYp, &destCb, &destCr, &info, permuteMap, kvImageDoNotTile); + if (error != kvImageNoError) { + return; + } + + vImageExtractChannel_ARGB8888(&src, &destA, 3, kvImageDoNotTile); +} + +void combineYUVAPlanesIntoARBB(uint8_t *argb, uint8_t const *inY, uint8_t const *inU, uint8_t const *inV, uint8_t const *inA, int width, int height, int bytesPerRow) { + static vImage_YpCbCrToARGB info; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + vImage_YpCbCrPixelRange pixelRange = (vImage_YpCbCrPixelRange){ 0, 128, 255, 255, 255, 1, 255, 0 }; + vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_709_2, &pixelRange, &info, kvImage420Yp8_Cb8_Cr8, kvImageARGB8888, 0); + }); + + vImage_Error error = kvImageNoError; + + vImage_Buffer destArgb; + destArgb.data = (void *)argb; + destArgb.width = width; + destArgb.height = height; + destArgb.rowBytes = bytesPerRow; + + vImage_Buffer srcYp; + srcYp.data = (void *)inY; + srcYp.width = width; + srcYp.height = height; + srcYp.rowBytes = width; + + vImage_Buffer srcCr; + srcCr.data = (void *)inU; + srcCr.width = width / 2; + srcCr.height = height / 2; + srcCr.rowBytes = width / 2; + + vImage_Buffer srcCb; + srcCb.data = (void *)inV; + srcCb.width = width / 2; + srcCb.height = height / 2; + srcCb.rowBytes = width / 2; + + vImage_Buffer srcA; + srcA.data = (void *)inA; + srcA.width = width; + srcA.height = height; + srcA.rowBytes = width; + + error = vImageConvert_420Yp8_Cb8_Cr8ToARGB8888(&srcYp, &srcCb, &srcCr, &destArgb, &info, permuteMap, 255, kvImageDoNotTile); + + error = vImageOverwriteChannels_ARGB8888(&srcA, &destArgb, &destArgb, 1 << 0, kvImageDoNotTile); +} diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 819d57826a..4b03fb865c 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -3,6 +3,7 @@ import UIKit import SwiftSignalKit import CryptoUtils import ManagedFile +import Compression public final class AnimationCacheItemFrame { public enum Format { @@ -25,25 +26,68 @@ public final class AnimationCacheItemFrame { public final class AnimationCacheItem { public let numFrames: Int private let getFrameImpl: (Int) -> AnimationCacheItemFrame? + private let getFrameIndexImpl: (Double) -> Int - public init(numFrames: Int, getFrame: @escaping (Int) -> AnimationCacheItemFrame?) { + public init(numFrames: Int, getFrame: @escaping (Int) -> AnimationCacheItemFrame?, getFrameIndexImpl: @escaping (Double) -> Int) { self.numFrames = numFrames self.getFrameImpl = getFrame + self.getFrameIndexImpl = getFrameIndexImpl } public func getFrame(index: Int) -> AnimationCacheItemFrame? { return self.getFrameImpl(index) } + + public func getFrame(at duration: Double) -> AnimationCacheItemFrame? { + let index = self.getFrameIndexImpl(duration) + return self.getFrameImpl(index) + } +} + +public struct AnimationCacheItemDrawingSurface { + public let argb: UnsafeMutablePointer + public let width: Int + public let height: Int + public let bytesPerRow: Int + public let length: Int + + init( + argb: UnsafeMutablePointer, + width: Int, + height: Int, + bytesPerRow: Int, + length: Int + ) { + self.argb = argb + self.width = width + self.height = height + self.bytesPerRow = bytesPerRow + self.length = length + } } public protocol AnimationCacheItemWriter: AnyObject { - func add(bytes: UnsafeRawPointer, length: Int, width: Int, height: Int, bytesPerRow: Int, duration: Double) + var queue: Queue { get } + var isCancelled: Bool { get } + + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Void, proposedWidth: Int, proposedHeight: Int, duration: Double) func finish() } +public final class AnimationCacheItemResult { + public let item: AnimationCacheItem? + public let isFinal: Bool + + public init(item: AnimationCacheItem?, isFinal: Bool) { + self.item = item + self.isFinal = isFinal + } +} + public protocol AnimationCache: AnyObject { - func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Signal - func getSynchronously(sourceId: String) -> AnimationCacheItem? + func get(sourceId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Signal + func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? + func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable } private func md5Hash(_ string: String) -> String { @@ -70,11 +114,82 @@ private func itemSubpath(hashString: String) -> (directory: String, fileName: St return (directory, hashString) } +private func roundUp(_ numToRound: Int, multiple: Int) -> Int { + if multiple == 0 { + return numToRound + } + + let remainder = numToRound % multiple + if remainder == 0 { + return numToRound; + } + + return numToRound + multiple - remainder +} + +private func compressData(data: Data, addSizeHeader: Bool = false) -> Data? { + let algorithm: compression_algorithm = COMPRESSION_LZFSE + + let scratchData = malloc(compression_encode_scratch_buffer_size(algorithm))! + defer { + free(scratchData) + } + + let headerSize = addSizeHeader ? 4 : 0 + var compressedData = Data(count: headerSize + data.count + 16 * 1024) + let resultSize = compressedData.withUnsafeMutableBytes { buffer -> Int in + guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return 0 + } + + if addSizeHeader { + var decompressedSize: UInt32 = UInt32(data.count) + memcpy(bytes, &decompressedSize, 4) + } + + return data.withUnsafeBytes { sourceBuffer -> Int in + return compression_encode_buffer(bytes.advanced(by: headerSize), buffer.count - headerSize, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), sourceBuffer.count, scratchData, algorithm) + } + } + + if resultSize <= 0 { + return nil + } + compressedData.count = headerSize + resultSize + return compressedData +} + +private func decompressData(data: Data, range: Range, decompressedSize: Int) -> Data? { + let algorithm: compression_algorithm = COMPRESSION_LZFSE + + let scratchData = malloc(compression_decode_scratch_buffer_size(algorithm))! + defer { + free(scratchData) + } + + var decompressedFrameData = Data(count: decompressedSize) + let resultSize = decompressedFrameData.withUnsafeMutableBytes { buffer -> Int in + guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return 0 + } + return data.withUnsafeBytes { sourceBuffer -> Int in + return compression_decode_buffer(bytes, buffer.count, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: range.lowerBound), range.upperBound - range.lowerBound, scratchData, algorithm) + } + } + + if resultSize <= 0 { + return nil + } + if decompressedFrameData.count != resultSize { + decompressedFrameData.count = resultSize + } + return decompressedFrameData +} + private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { - private struct ParameterSet: Equatable { - var width: Int - var height: Int - var bytesPerRow: Int + struct CompressedResult { + var animationPath: String + var firstFramePath: String } private struct FrameMetadata { @@ -83,10 +198,19 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { var duration: Double } - private let file: ManagedFile - private let completion: (Bool) -> Void + let queue: Queue + var isCancelled: Bool = false - private var currentParameterSet: ParameterSet? + private let decompressedPath: String + private let compressedPath: String + private let firstFramePath: String + private var file: ManagedFile? + private let completion: (CompressedResult?) -> Void + + private var currentSurface: ImageARGB? + private var currentYUVASurface: ImageYUVA420? + private var currentDctData: DctData? + private var currentDctCoefficients: DctCoefficientsYUVA420? private var contentLengthOffset: Int? private var isFailed: Bool = false private var isFinished: Bool = false @@ -94,44 +218,141 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { private var frames: [FrameMetadata] = [] private var contentLength: Int = 0 + private let dctQuality: Int + private let lock = Lock() - init?(tempPath: String, completion: @escaping (Bool) -> Void) { - guard let file = ManagedFile(queue: nil, path: tempPath, mode: .readwrite) else { + init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { + self.dctQuality = 67 + + self.queue = queue + self.decompressedPath = allocateTempFile() + self.compressedPath = allocateTempFile() + self.firstFramePath = allocateTempFile() + + guard let file = ManagedFile(queue: nil, path: self.decompressedPath, mode: .readwrite) else { return nil } self.file = file self.completion = completion } - func add(bytes: UnsafeRawPointer, length: Int, width: Int, height: Int, bytesPerRow: Int, duration: Double) { + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Void, proposedWidth: Int, proposedHeight: Int, duration: Double) { + if self.isFailed || self.isFinished { + return + } + self.lock.locked { - if self.isFailed { + guard !self.isFailed, !self.isFinished, let file = self.file else { return } - let parameterSet = ParameterSet(width: width, height: height, bytesPerRow: bytesPerRow) - if let currentParameterSet = self.currentParameterSet { - if currentParameterSet != parameterSet { + let width = roundUp(proposedWidth, multiple: 16) + let height = roundUp(proposedWidth, multiple: 16) + + var isFirstFrame = false + + let surface: ImageARGB + if let current = self.currentSurface { + if current.argbPlane.width == width && current.argbPlane.height == height { + surface = current + } else { self.isFailed = true return } } else { - self.currentParameterSet = parameterSet + isFirstFrame = true - self.file.write(1 as UInt32) - - self.file.write(UInt32(parameterSet.width)) - self.file.write(UInt32(parameterSet.height)) - self.file.write(UInt32(parameterSet.bytesPerRow)) - - self.contentLengthOffset = Int(self.file.position()) - self.file.write(0 as UInt32) + surface = ImageARGB(width: width, height: height) + self.currentSurface = surface } - self.frames.append(FrameMetadata(offset: Int(self.file.position()), length: length, duration: duration)) - let _ = self.file.write(bytes, count: length) - self.contentLength += length + let yuvaSurface: ImageYUVA420 + if let current = self.currentYUVASurface { + if current.yPlane.width == width && current.yPlane.height == height { + yuvaSurface = current + } else { + self.isFailed = true + return + } + } else { + yuvaSurface = ImageYUVA420(width: width, height: height) + self.currentYUVASurface = yuvaSurface + } + + let dctCoefficients: DctCoefficientsYUVA420 + if let current = self.currentDctCoefficients { + if current.yPlane.width == width && current.yPlane.height == height { + dctCoefficients = current + } else { + self.isFailed = true + return + } + } else { + dctCoefficients = DctCoefficientsYUVA420(width: width, height: height) + self.currentDctCoefficients = dctCoefficients + } + + let dctData: DctData + if let current = self.currentDctData, current.quality == self.dctQuality { + dctData = current + } else { + dctData = DctData(quality: self.dctQuality) + self.currentDctData = dctData + } + + surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Void in + drawingBlock(AnimationCacheItemDrawingSurface( + argb: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), + width: width, + height: height, + bytesPerRow: surface.argbPlane.bytesPerRow, + length: bytes.count + )) + } + + surface.toYUVA420(target: yuvaSurface) + yuvaSurface.dct(dctData: dctData, target: dctCoefficients) + + if isFirstFrame { + file.write(2 as UInt32) + + file.write(UInt32(dctCoefficients.yPlane.width)) + file.write(UInt32(dctCoefficients.yPlane.height)) + file.write(UInt32(dctData.quality)) + + self.contentLengthOffset = Int(file.position()) + file.write(0 as UInt32) + } + + let framePosition = Int(file.position()) + assert(framePosition >= 0) + var frameLength = 0 + + for i in 0 ..< 4 { + let dctPlane: DctCoefficientPlane + switch i { + case 0: + dctPlane = dctCoefficients.yPlane + case 1: + dctPlane = dctCoefficients.uPlane + case 2: + dctPlane = dctCoefficients.vPlane + case 3: + dctPlane = dctCoefficients.aPlane + default: + preconditionFailure() + } + + dctPlane.data.withUnsafeBytes { bytes in + let _ = file.write(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) + } + frameLength += dctPlane.data.count + } + + self.frames.append(FrameMetadata(offset: framePosition, length: frameLength, duration: duration)) + + self.contentLength += frameLength } } @@ -142,27 +363,96 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { self.isFinished = true shouldComplete = true - guard let contentLengthOffset = self.contentLengthOffset else { + guard let contentLengthOffset = self.contentLengthOffset, let file = self.file else { + self.isFailed = true + return + } + assert(contentLengthOffset >= 0) + + let metadataPosition = file.position() + file.seek(position: Int64(contentLengthOffset)) + file.write(UInt32(self.contentLength)) + + file.seek(position: metadataPosition) + file.write(UInt32(self.frames.count)) + for frame in self.frames { + file.write(UInt32(frame.offset)) + file.write(UInt32(frame.length)) + file.write(Float32(frame.duration)) + } + + if !self.frames.isEmpty, let dctCoefficients = self.currentDctCoefficients, let dctData = self.currentDctData { + var firstFrameData = Data(capacity: 4 * 5 + self.frames[0].length) + + writeUInt32(data: &firstFrameData, value: 2 as UInt32) + writeUInt32(data: &firstFrameData, value: UInt32(dctCoefficients.yPlane.width)) + writeUInt32(data: &firstFrameData, value: UInt32(dctCoefficients.yPlane.height)) + writeUInt32(data: &firstFrameData, value: UInt32(dctData.quality)) + + writeUInt32(data: &firstFrameData, value: UInt32(self.frames[0].length)) + let firstFrameStart = 4 * 5 + + file.seek(position: Int64(self.frames[0].offset)) + firstFrameData.count += self.frames[0].length + firstFrameData.withUnsafeMutableBytes { bytes in + let _ = file.read(bytes.baseAddress!.advanced(by: 4 * 5), self.frames[0].length) + } + + writeUInt32(data: &firstFrameData, value: UInt32(1)) + writeUInt32(data: &firstFrameData, value: UInt32(firstFrameStart)) + writeUInt32(data: &firstFrameData, value: UInt32(self.frames[0].length)) + writeFloat32(data: &firstFrameData, value: Float32(1.0)) + + guard let compressedFirstFrameData = compressData(data: firstFrameData, addSizeHeader: true) else { + self.isFailed = true + return + } + guard let _ = try? compressedFirstFrameData.write(to: URL(fileURLWithPath: self.firstFramePath)) else { + self.isFailed = true + return + } + } else { self.isFailed = true return } - let metadataPosition = self.file.position() - self.file.seek(position: Int64(contentLengthOffset)) - self.file.write(UInt32(self.contentLength)) - - self.file.seek(position: metadataPosition) - self.file.write(UInt32(self.frames.count)) - for frame in self.frames { - self.file.write(UInt32(frame.offset)) - self.file.write(UInt32(frame.length)) - self.file.write(Float32(frame.duration)) + if !self.isFailed { + self.file = nil + + file._unsafeClose() + + guard let uncompressedData = try? Data(contentsOf: URL(fileURLWithPath: self.decompressedPath), options: .alwaysMapped) else { + self.isFailed = true + return + } + guard let compressedData = compressData(data: uncompressedData) else { + self.isFailed = true + return + } + guard let compressedFile = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { + self.isFailed = true + return + } + compressedFile.write(Int32(uncompressedData.count)) + let _ = compressedFile.write(compressedData) + compressedFile._unsafeClose() } } } if shouldComplete { - self.completion(!self.isFailed) + let _ = try? FileManager.default.removeItem(atPath: self.decompressedPath) + + if !self.isFailed { + self.completion(CompressedResult( + animationPath: self.compressedPath, + firstFramePath: self.firstFramePath + )) + } else { + let _ = try? FileManager.default.removeItem(atPath: self.compressedPath) + let _ = try? FileManager.default.removeItem(atPath: self.firstFramePath) + self.completion(nil) + } } } } @@ -175,12 +465,34 @@ private final class AnimationCacheItemAccessor { private let data: Data private let frameMapping: [Int: FrameInfo] - private let format: AnimationCacheItemFrame.Format + private let durationMapping: [Double] + private let totalDuration: Double - init(data: Data, frameMapping: [Int: FrameInfo], format: AnimationCacheItemFrame.Format) { + private var currentYUVASurface: ImageYUVA420 + private var currentDctData: DctData + private var currentDctCoefficients: DctCoefficientsYUVA420 + + init(data: Data, frameMapping: [FrameInfo], width: Int, height: Int, dctQuality: Int) { self.data = data - self.frameMapping = frameMapping - self.format = format + + var resultFrameMapping: [Int: FrameInfo] = [:] + var durationMapping: [Double] = [] + var totalDuration: Double = 0.0 + + for i in 0 ..< frameMapping.count { + let frame = frameMapping[i] + resultFrameMapping[i] = frame + totalDuration += frame.duration + durationMapping.append(totalDuration) + } + + self.frameMapping = resultFrameMapping + self.durationMapping = durationMapping + self.totalDuration = totalDuration + + self.currentYUVASurface = ImageYUVA420(width: width, height: height) + self.currentDctData = DctData(quality: dctQuality) + self.currentDctCoefficients = DctCoefficientsYUVA420(width: width, height: height) } func getFrame(index: Int) -> AnimationCacheItemFrame? { @@ -188,7 +500,56 @@ private final class AnimationCacheItemAccessor { return nil } - return AnimationCacheItemFrame(data: data, range: frameInfo.range, format: self.format, duration: frameInfo.duration) + let currentSurface = ImageARGB(width: self.currentYUVASurface.yPlane.width, height: self.currentYUVASurface.yPlane.height) + + var frameDataOffset = 0 + let frameLength = frameInfo.range.upperBound - frameInfo.range.lowerBound + for i in 0 ..< 4 { + let dctPlane: DctCoefficientPlane + switch i { + case 0: + dctPlane = self.currentDctCoefficients.yPlane + case 1: + dctPlane = self.currentDctCoefficients.uPlane + case 2: + dctPlane = self.currentDctCoefficients.vPlane + case 3: + dctPlane = self.currentDctCoefficients.aPlane + default: + preconditionFailure() + } + + if frameDataOffset + dctPlane.data.count > frameLength { + break + } + + dctPlane.data.withUnsafeMutableBytes { targetBuffer -> Void in + self.data.copyBytes(to: targetBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), from: (frameInfo.range.lowerBound + frameDataOffset) ..< (frameInfo.range.lowerBound + frameDataOffset + targetBuffer.count)) + } + + frameDataOffset += dctPlane.data.count + } + + self.currentDctCoefficients.idct(dctData: self.currentDctData, target: self.currentYUVASurface) + self.currentYUVASurface.toARGB(target: currentSurface) + + return AnimationCacheItemFrame(data: currentSurface.argbPlane.data, range: 0 ..< currentSurface.argbPlane.data.count, format: .rgba(width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: frameInfo.duration) + } + + func getFrameIndex(duration: Double) -> Int { + if self.totalDuration == 0.0 { + return 0 + } + if self.durationMapping.count <= 1 { + return 0 + } + let normalizedDuration = duration.truncatingRemainder(dividingBy: self.totalDuration) + for i in 1 ..< self.durationMapping.count { + if normalizedDuration < self.durationMapping[i] { + return i - 1 + } + } + return self.durationMapping.count - 1 } } @@ -203,10 +564,54 @@ private func readUInt32(data: Data, offset: Int) -> UInt32 { return value } +private func readFloat32(data: Data, offset: Int) -> Float32 { + var value: Float32 = 0 + withUnsafeMutableBytes(of: &value, { bytes -> Void in + data.withUnsafeBytes { dataBytes -> Void in + memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) + } + }) + + return value +} + +private func writeUInt32(data: inout Data, value: UInt32) { + var value: UInt32 = value + withUnsafeBytes(of: &value, { bytes -> Void in + data.count += 4 + data.withUnsafeMutableBytes { dataBytes -> Void in + memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) + } + }) +} + +private func writeFloat32(data: inout Data, value: Float32) { + var value: Float32 = value + withUnsafeBytes(of: &value, { bytes -> Void in + data.count += 4 + data.withUnsafeMutableBytes { dataBytes -> Void in + memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) + } + }) +} + private func loadItem(path: String) -> AnimationCacheItem? { - guard let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else { + guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else { return nil } + + if compressedData.count < 4 { + return nil + } + let decompressedSize = readUInt32(data: compressedData, offset: 0) + + if decompressedSize <= 0 || decompressedSize > 20 * 1024 * 1024 { + return nil + } + guard let data = decompressData(data: compressedData, range: 4 ..< compressedData.count, decompressedSize: Int(decompressedSize)) else { + return nil + } + let dataLength = data.count var offset = 0 @@ -216,7 +621,7 @@ private func loadItem(path: String) -> AnimationCacheItem? { } let formatVersion = readUInt32(data: data, offset: offset) offset += 4 - if formatVersion != 1 { + if formatVersion != 2 { return nil } @@ -235,7 +640,7 @@ private func loadItem(path: String) -> AnimationCacheItem? { guard dataLength >= offset + 4 else { return nil } - let bytesPerRow = readUInt32(data: data, offset: offset) + let dctQuality = readUInt32(data: data, offset: offset) offset += 4 guard dataLength >= offset + 4 else { @@ -252,8 +657,8 @@ private func loadItem(path: String) -> AnimationCacheItem? { let numFrames = readUInt32(data: data, offset: offset) offset += 4 - var frameMapping: [Int: AnimationCacheItemAccessor.FrameInfo] = [:] - for i in 0 ..< Int(numFrames) { + var frameMapping: [AnimationCacheItemAccessor.FrameInfo] = [] + for _ in 0 ..< Int(numFrames) { guard dataLength >= offset + 4 + 4 + 4 else { return nil } @@ -262,23 +667,25 @@ private func loadItem(path: String) -> AnimationCacheItem? { offset += 4 let frameLength = readUInt32(data: data, offset: offset) offset += 4 - let frameDuration = readUInt32(data: data, offset: offset) + let frameDuration = readFloat32(data: data, offset: offset) offset += 4 - frameMapping[i] = AnimationCacheItemAccessor.FrameInfo(range: Int(frameStart) ..< Int(frameStart + frameLength), duration: Double(frameDuration)) + frameMapping.append(AnimationCacheItemAccessor.FrameInfo(range: Int(frameStart) ..< Int(frameStart + frameLength), duration: Double(frameDuration))) } - let itemAccessor = AnimationCacheItemAccessor(data: data, frameMapping: frameMapping, format: .rgba(width: Int(width), height: Int(height), bytesPerRow: Int(bytesPerRow))) + let itemAccessor = AnimationCacheItemAccessor(data: data, frameMapping: frameMapping, width: Int(width), height: Int(height), dctQuality: Int(dctQuality)) return AnimationCacheItem(numFrames: Int(numFrames), getFrame: { index in return itemAccessor.getFrame(index: index) + }, getFrameIndexImpl: { duration in + return itemAccessor.getFrameIndex(duration: duration) }) } public final class AnimationCacheImpl: AnimationCache { private final class Impl { private final class ItemContext { - let subscribers = Bag<(AnimationCacheItem?) -> Void>() + let subscribers = Bag<(AnimationCacheItemResult) -> Void>() let disposable = MetaDisposable() deinit { @@ -290,10 +697,22 @@ public final class AnimationCacheImpl: AnimationCache { private let basePath: String private let allocateTempFile: () -> String + private let fetchQueues: [Queue] + private var nextFetchQueueIndex: Int = 0 + private var itemContexts: [String: ItemContext] = [:] init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String) { self.queue = queue + + let fetchQueueCount: Int + if ProcessInfo.processInfo.activeProcessorCount > 2 { + fetchQueueCount = 3 + } else { + fetchQueueCount = 2 + } + + self.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "AnimationCacheImpl-Fetch\(i)", qos: .default) } self.basePath = basePath self.allocateTempFile = allocateTempFile } @@ -301,13 +720,14 @@ public final class AnimationCacheImpl: AnimationCache { deinit { } - func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable { - let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId)) + func get(sourceId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))")) let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - if FileManager.default.fileExists(atPath: itemPath) { - completion(loadItem(path: itemPath)) + if FileManager.default.fileExists(atPath: itemPath), let item = loadItem(path: itemPath) { + updateResult(AnimationCacheItemResult(item: item, isFinal: true)) return EmptyDisposable } @@ -323,11 +743,14 @@ public final class AnimationCacheImpl: AnimationCache { } let queue = self.queue - let index = itemContext.subscribers.add(completion) + let index = itemContext.subscribers.add(updateResult) + + updateResult(AnimationCacheItemResult(item: nil, isFinal: false)) if beginFetch { - let tempPath = self.allocateTempFile() - guard let writer = AnimationCacheItemWriterImpl(tempPath: tempPath, completion: { [weak self, weak itemContext] success in + let fetchQueueIndex = self.nextFetchQueueIndex + self.nextFetchQueueIndex += 1 + guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in queue.async { guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[sourceId] else { return @@ -335,13 +758,18 @@ public final class AnimationCacheImpl: AnimationCache { strongSelf.itemContexts.removeValue(forKey: sourceId) - guard success else { + guard let result = result else { return } guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { return } - guard let _ = try? FileManager.default.moveItem(atPath: tempPath, toPath: itemPath) else { + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { + return + } + let _ = try? FileManager.default.removeItem(atPath: itemFirstFramePath) + guard let _ = try? FileManager.default.moveItem(atPath: result.firstFramePath, toPath: itemFirstFramePath) else { return } guard let item = loadItem(path: itemPath) else { @@ -349,16 +777,21 @@ public final class AnimationCacheImpl: AnimationCache { } for f in itemContext.subscribers.copyItems() { - f(item) + f(AnimationCacheItemResult(item: item, isFinal: true)) } } }) else { return EmptyDisposable } - let fetchDisposable = fetch(writer) + let fetchDisposable = MetaDisposable() + fetchDisposable.set(fetch(size, writer)) - itemContext.disposable.set(ActionDisposable { + itemContext.disposable.set(ActionDisposable { [weak writer] in + if let writer = writer { + writer.isCancelled = true + } + fetchDisposable.dispose() }) } @@ -377,38 +810,58 @@ public final class AnimationCacheImpl: AnimationCache { } } - func getSynchronously(sourceId: String) -> AnimationCacheItem? { - let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId)) - let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" - let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" + static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize) -> AnimationCacheItem? { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))")) + let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - if FileManager.default.fileExists(atPath: itemPath) { - return loadItem(path: itemPath) + if FileManager.default.fileExists(atPath: itemFirstFramePath) { + return loadItem(path: itemFirstFramePath) } else { return nil } } + + static func getFirstFrame(basePath: String, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))")) + let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" + + if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = loadItem(path: itemFirstFramePath) { + completion(item) + + return EmptyDisposable + } else { + completion(nil) + + return EmptyDisposable + } + } } private let queue: Queue + private let basePath: String private let impl: QueueLocalObject public init(basePath: String, allocateTempFile: @escaping () -> String) { let queue = Queue() self.queue = queue + self.basePath = basePath self.impl = QueueLocalObject(queue: queue, generate: { return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile) }) } - public func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Signal { + public func get(sourceId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in - disposable.set(impl.get(sourceId: sourceId, fetch: fetch, completion: { result in + disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { result in subscriber.putNext(result) - subscriber.putCompletion() + if result.isFinal { + subscriber.putCompletion() + } })) } @@ -417,9 +870,18 @@ public final class AnimationCacheImpl: AnimationCache { |> runOn(self.queue) } - public func getSynchronously(sourceId: String) -> AnimationCacheItem? { - return self.impl.syncWith { impl -> AnimationCacheItem? in - return impl.getSynchronously(sourceId: sourceId) + public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { + return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size) + } + + public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable { + let disposable = MetaDisposable() + + let basePath = self.basePath + queue.async { + disposable.set(Impl.getFirstFrame(basePath: basePath, sourceId: sourceId, size: size, completion: completion)) } + + return disposable } } diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift new file mode 100644 index 0000000000..a30d8fd48f --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift @@ -0,0 +1,231 @@ +import Foundation +import UIKit +import DCT + +final class ImagePlane { + let width: Int + let height: Int + let bytesPerRow: Int + let components: Int + var data: Data + + init(width: Int, height: Int, components: Int) { + self.width = width + self.height = height + self.bytesPerRow = width * components + self.components = components + self.data = Data(count: width * components * height) + } +} + +final class ImageARGB { + let argbPlane: ImagePlane + + init(width: Int, height: Int) { + self.argbPlane = ImagePlane(width: width, height: height, components: 4) + } +} + +final class ImageYUVA420 { + let yPlane: ImagePlane + let uPlane: ImagePlane + let vPlane: ImagePlane + let aPlane: ImagePlane + + init(width: Int, height: Int) { + self.yPlane = ImagePlane(width: width, height: height, components: 1) + self.uPlane = ImagePlane(width: width / 2, height: height / 2, components: 1) + self.vPlane = ImagePlane(width: width / 2, height: height / 2, components: 1) + self.aPlane = ImagePlane(width: width, height: height, components: 1) + } +} + +final class DctCoefficientPlane { + let width: Int + let height: Int + var data: Data + + init(width: Int, height: Int) { + self.width = width + self.height = height + self.data = Data(count: width * 2 * height) + } +} + +final class DctCoefficientsYUVA420 { + let yPlane: DctCoefficientPlane + let uPlane: DctCoefficientPlane + let vPlane: DctCoefficientPlane + let aPlane: DctCoefficientPlane + + init(width: Int, height: Int) { + self.yPlane = DctCoefficientPlane(width: width, height: height) + self.uPlane = DctCoefficientPlane(width: width / 2, height: height / 2) + self.vPlane = DctCoefficientPlane(width: width / 2, height: height / 2) + self.aPlane = DctCoefficientPlane(width: width, height: height) + } +} + +extension ImageARGB { + func toYUVA420(target: ImageYUVA420) { + precondition(self.argbPlane.width == target.yPlane.width && self.argbPlane.height == target.yPlane.height) + + self.argbPlane.data.withUnsafeBytes { argbBuffer -> Void in + target.yPlane.data.withUnsafeMutableBytes { yBuffer -> Void in + target.uPlane.data.withUnsafeMutableBytes { uBuffer -> Void in + target.vPlane.data.withUnsafeMutableBytes { vBuffer -> Void in + target.aPlane.data.withUnsafeMutableBytes { aBuffer -> Void in + splitRGBAIntoYUVAPlanes( + argbBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + yBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + uBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + vBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + aBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + Int32(self.argbPlane.width), + Int32(self.argbPlane.height), + Int32(self.argbPlane.bytesPerRow) + ) + } + } + } + } + } + } + + func toYUVA420() -> ImageYUVA420 { + let resultImage = ImageYUVA420(width: self.argbPlane.width, height: self.argbPlane.height) + self.toYUVA420(target: resultImage) + return resultImage + } +} + +extension ImageYUVA420 { + func toARGB(target: ImageARGB) { + precondition(self.yPlane.width == target.argbPlane.width && self.yPlane.height == target.argbPlane.height) + + self.yPlane.data.withUnsafeBytes { yBuffer -> Void in + self.uPlane.data.withUnsafeBytes { uBuffer -> Void in + self.vPlane.data.withUnsafeBytes { vBuffer -> Void in + self.aPlane.data.withUnsafeBytes { aBuffer -> Void in + target.argbPlane.data.withUnsafeMutableBytes { argbBuffer -> Void in + combineYUVAPlanesIntoARBB( + argbBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + yBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + uBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + vBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + aBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), + Int32(target.argbPlane.width), + Int32(target.argbPlane.height), + Int32(target.argbPlane.bytesPerRow) + ) + } + } + } + } + } + } + + func toARGB() -> ImageARGB { + let resultImage = ImageARGB(width: self.yPlane.width, height: self.yPlane.height) + self.toARGB(target: resultImage) + return resultImage + } +} + +final class DctData { + let quality: Int + let dctData: Data + let idctData: Data + + init(quality: Int) { + self.quality = quality + self.dctData = generateForwardDctData(Int32(quality))! + self.idctData = generateInverseDctData(Int32(quality))! + } +} + +extension ImageYUVA420 { + func dct(dctData: DctData, target: DctCoefficientsYUVA420) { + precondition(self.yPlane.width == target.yPlane.width && self.yPlane.height == target.yPlane.height) + + for i in 0 ..< 4 { + let sourcePlane: ImagePlane + let targetPlane: DctCoefficientPlane + switch i { + case 0: + sourcePlane = self.yPlane + targetPlane = target.yPlane + case 1: + sourcePlane = self.uPlane + targetPlane = target.uPlane + case 2: + sourcePlane = self.vPlane + targetPlane = target.vPlane + case 3: + sourcePlane = self.aPlane + targetPlane = target.aPlane + default: + preconditionFailure() + } + + sourcePlane.data.withUnsafeBytes { sourceBytes in + let sourcePixels = sourceBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + + targetPlane.data.withUnsafeMutableBytes { bytes in + let coefficients = bytes.baseAddress!.assumingMemoryBound(to: UInt16.self) + + performForwardDct(sourcePixels, coefficients, Int32(sourcePlane.width), Int32(sourcePlane.height), Int32(sourcePlane.bytesPerRow), dctData.dctData) + } + } + } + } + + func dct(dctData: DctData) -> DctCoefficientsYUVA420 { + let results = DctCoefficientsYUVA420(width: self.yPlane.width, height: self.yPlane.height) + self.dct(dctData: dctData, target: results) + return results + } +} + +extension DctCoefficientsYUVA420 { + func idct(dctData: DctData, target: ImageYUVA420) { + precondition(self.yPlane.width == target.yPlane.width && self.yPlane.height == target.yPlane.height) + + for i in 0 ..< 4 { + let sourcePlane: DctCoefficientPlane + let targetPlane: ImagePlane + switch i { + case 0: + sourcePlane = self.yPlane + targetPlane = target.yPlane + case 1: + sourcePlane = self.uPlane + targetPlane = target.uPlane + case 2: + sourcePlane = self.vPlane + targetPlane = target.vPlane + case 3: + sourcePlane = self.aPlane + targetPlane = target.aPlane + default: + preconditionFailure() + } + + sourcePlane.data.withUnsafeBytes { sourceBytes in + let coefficients = sourceBytes.baseAddress!.assumingMemoryBound(to: UInt16.self) + + targetPlane.data.withUnsafeMutableBytes { bytes in + let pixels = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + + performInverseDct(coefficients, pixels, Int32(sourcePlane.width), Int32(sourcePlane.height), Int32(targetPlane.bytesPerRow), Int32(sourcePlane.width), dctData.idctData) + } + } + } + } + + func idct(dctData: DctData) -> ImageYUVA420 { + let resultImage = ImageYUVA420(width: self.yPlane.width, height: self.yPlane.height) + self.idct(dctData: dctData, target: resultImage) + return resultImage + } +} diff --git a/submodules/TelegramUI/Components/EmojiKeyboard/Sources/EmojiKeyboard.swift b/submodules/TelegramUI/Components/EmojiKeyboard/Sources/EmojiKeyboard.swift deleted file mode 100644 index 2cc37ab552..0000000000 --- a/submodules/TelegramUI/Components/EmojiKeyboard/Sources/EmojiKeyboard.swift +++ /dev/null @@ -1,4 +0,0 @@ -import Foundation -import UIKit -import Display - diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD index ff1faa52e0..37de259bd2 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", + "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/ShimmerEffect:ShimmerEffect", ], diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 700adef954..eecc39922f 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -11,25 +11,39 @@ import TelegramCore import Postbox import AnimationCache import LottieAnimationCache +import VideoAnimationCache import MultiAnimationRenderer import ShimmerEffect +import TextFormat public final class InlineStickerItemLayer: MultiAnimationRenderTarget { public static let queue = Queue() public struct Key: Hashable { - public var id: MediaId + public var id: Int64 public var index: Int - public init(id: MediaId, index: Int) { + public init(id: Int64, index: Int) { self.id = id self.index = index } } - private let file: TelegramMediaFile + private let context: AccountContext + private let groupId: String + private let emoji: ChatTextInputTextCustomEmojiAttribute + private let cache: AnimationCache + private let renderer: MultiAnimationRenderer + private let placeholderColor: UIColor + + private let pointSize: CGSize + private let pixelSize: CGSize + + private var file: TelegramMediaFile? + private var infoDisposable: Disposable? private var disposable: Disposable? private var fetchDisposable: Disposable? + private var loadDisposable: Disposable? private var isInHierarchyValue: Bool = false public var isVisibleForAnimations: Bool = false { @@ -39,43 +53,36 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } } - private var displayLink: ConstantDisplayLinkAnimator? - public init(context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) { - self.file = file + public init(context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) { + self.context = context + self.groupId = groupId + self.emoji = emoji + self.cache = cache + self.renderer = renderer + self.placeholderColor = placeholderColor + + let scale = min(2.0, UIScreenScale) + self.pointSize = CGSize(width: 24, height: 24) + self.pixelSize = CGSize(width: self.pointSize.width * scale, height: self.pointSize.height * scale) super.init() - if attemptSynchronousLoad { - if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation) { - let size = CGSize(width: 24.0, height: 24.0) - if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { - self.contents = image.cgImage - } + self.infoDisposable = (context.engine.stickers.loadedStickerPack(reference: emoji.stickerPack, forceActualized: false) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return } - } - - self.disposable = renderer.add(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, fetch: { writer in - let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) - - let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in - guard let result = result else { - return + switch result { + case let .result(_, items, _): + for item in items { + if item.file.fileId.id == emoji.fileId { + strongSelf.updateFile(file: item.file, attemptSynchronousLoad: false) + break + } } - - guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { - writer.finish() - return - } - let scale = min(2.0, UIScreenScale) - cacheLottieAnimation(data: data, width: Int(24 * scale), height: Int(24 * scale), writer: writer) - }) - - let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start() - - return ActionDisposable { - dataDisposable.dispose() - fetchDisposable.dispose() + default: + break } }) } @@ -89,6 +96,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } deinit { + self.loadDisposable?.dispose() + self.infoDisposable?.dispose() self.disposable?.dispose() self.fetchDisposable?.dispose() } @@ -108,13 +117,70 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.shouldBeAnimating = shouldBePlaying } + + private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { + if self.file?.fileId == file.fileId { + return + } + + self.file = file + + if attemptSynchronousLoad { + if !self.renderer.loadFirstFrameSynchronously(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize) { + if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: self.placeholderColor) { + self.contents = image.cgImage + } + } + + self.loadAnimation() + } else { + self.loadDisposable = self.renderer.loadFirstFrame(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, completion: { [weak self] _ in + self?.loadAnimation() + }) + self.loadAnimation() + } + } + + private func loadAnimation() { + guard let file = self.file else { + return + } + + let context = self.context + self.disposable = renderer.add(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in + let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) + + let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in + guard let result = result else { + return + } + + if file.isVideoSticker { + cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer) + } else { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { + writer.finish() + return + } + cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) + } + }) + + let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: file.resource).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() + } + }) + } } public final class EmojiTextAttachmentView: UIView { private let contentLayer: InlineStickerItemLayer - public init(context: AccountContext, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) { - self.contentLayer = InlineStickerItemLayer(context: context, groupId: "textInputView", attemptSynchronousLoad: true, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor) + public init(context: AccountContext, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) { + self.contentLayer = InlineStickerItemLayer(context: context, groupId: "textInputView", attemptSynchronousLoad: true, emoji: emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor) super.init(frame: CGRect()) @@ -129,6 +195,6 @@ public final class EmojiTextAttachmentView: UIView { 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)) + self.contentLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height)) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD new file mode 100644 index 0000000000..5947ec2a2e --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -0,0 +1,40 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "EntityKeyboard", + module_name = "EntityKeyboard", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/PagerComponent:PagerComponent", + "//submodules/Components/BlurredBackgroundComponent:BlurredBackgroundComponent", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + "//submodules/Components/BundleIconComponent:BundleIconComponent", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramCore:TelegramCore", + "//submodules/Postbox:Postbox", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/YuvConversion:YuvConversion", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", + "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/MultiVideoRenderer:MultiVideoRenderer", + "//submodules/SoftwareVideo:SoftwareVideo", + "//submodules/ShimmerEffect:ShimmerEffect", + "//submodules/PhotoResources:PhotoResources", + "//submodules/StickerResources:StickerResources", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift new file mode 100644 index 0000000000..5c03454d8a --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -0,0 +1,678 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox +import MultiAnimationRenderer +import AnimationCache +import AccountContext +import LottieAnimationCache +import VideoAnimationCache +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import SwiftSignalKit +import ShimmerEffect +import PagerComponent +import StickerResources + +public final class EmojiPagerContentComponent: Component { + public typealias EnvironmentType = (EntityKeyboardChildEnvironment, PagerComponentChildEnvironment) + + public final class InputInteraction { + public let performItemAction: (Item, UIView, CGRect, CALayer) -> Void + public let deleteBackwards: () -> Void + public let openStickerSettings: () -> Void + + public init( + performItemAction: @escaping (Item, UIView, CGRect, CALayer) -> Void, + deleteBackwards: @escaping () -> Void, + openStickerSettings: @escaping () -> Void + ) { + self.performItemAction = performItemAction + self.deleteBackwards = deleteBackwards + self.openStickerSettings = openStickerSettings + } + } + + public final class Item: Equatable { + public let emoji: String + public let file: TelegramMediaFile + + public init(emoji: String, file: TelegramMediaFile) { + self.emoji = emoji + self.file = file + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if lhs.emoji != rhs.emoji { + return false + } + if lhs.file.fileId != rhs.file.fileId { + return false + } + + return true + } + } + + public final class ItemGroup: Equatable { + public let id: AnyHashable + public let title: String? + public let items: [Item] + + public init( + id: AnyHashable, + title: String?, + items: [Item] + ) { + self.id = id + self.title = title + self.items = items + } + + public static func ==(lhs: ItemGroup, rhs: ItemGroup) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + } + + public enum ItemLayoutType { + case compact + case detailed + } + + public let context: AccountContext + public let animationCache: AnimationCache + public let animationRenderer: MultiAnimationRenderer + public let inputInteraction: InputInteraction + public let itemGroups: [ItemGroup] + public let itemLayoutType: ItemLayoutType + + public init( + context: AccountContext, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + inputInteraction: InputInteraction, + itemGroups: [ItemGroup], + itemLayoutType: ItemLayoutType + ) { + self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer + self.inputInteraction = inputInteraction + self.itemGroups = itemGroups + self.itemLayoutType = itemLayoutType + } + + public static func ==(lhs: EmojiPagerContentComponent, rhs: EmojiPagerContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.animationCache !== rhs.animationCache { + return false + } + if lhs.animationRenderer !== rhs.animationRenderer { + return false + } + if lhs.inputInteraction !== rhs.inputInteraction { + return false + } + if lhs.itemGroups != rhs.itemGroups { + return false + } + if lhs.itemLayoutType != rhs.itemLayoutType { + return false + } + + return true + } + + public final class View: UIView, UIScrollViewDelegate { + private struct ItemGroupDescription: Equatable { + let hasTitle: Bool + let itemCount: Int + } + + private struct ItemGroupLayout: Equatable { + let frame: CGRect + let itemTopOffset: CGFloat + let itemCount: Int + } + + private struct ItemLayout: Equatable { + var width: CGFloat + var containerInsets: UIEdgeInsets + var itemGroupLayouts: [ItemGroupLayout] + var itemSize: CGFloat + var horizontalSpacing: CGFloat + var verticalSpacing: CGFloat + var verticalGroupSpacing: CGFloat + var itemsPerRow: Int + var contentSize: CGSize + + init(width: CGFloat, containerInsets: UIEdgeInsets, itemGroups: [ItemGroupDescription], itemLayoutType: ItemLayoutType) { + self.width = width + self.containerInsets = containerInsets + + let minSpacing: CGFloat + switch itemLayoutType { + case .compact: + self.itemSize = 36.0 + self.verticalSpacing = 9.0 + minSpacing = 9.0 + case .detailed: + self.itemSize = 60.0 + self.verticalSpacing = 9.0 + minSpacing = 9.0 + } + + self.verticalGroupSpacing = 18.0 + + let itemHorizontalSpace = width - self.containerInsets.left - self.containerInsets.right + + self.itemsPerRow = Int((itemHorizontalSpace + minSpacing) / (self.itemSize + minSpacing)) + self.horizontalSpacing = floor((itemHorizontalSpace - self.itemSize * CGFloat(self.itemsPerRow)) / CGFloat(self.itemsPerRow - 1)) + + var verticalGroupOrigin: CGFloat = self.containerInsets.top + self.itemGroupLayouts = [] + for itemGroup in itemGroups { + var itemTopOffset: CGFloat = 0.0 + if itemGroup.hasTitle { + itemTopOffset += 24.0 + } + + let numRowsInGroup = (itemGroup.itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow + let groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.itemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) + self.itemGroupLayouts.append(ItemGroupLayout( + frame: CGRect(origin: CGPoint(x: 0.0, y: verticalGroupOrigin), size: groupContentSize), + itemTopOffset: itemTopOffset, + itemCount: itemGroup.itemCount + )) + verticalGroupOrigin += groupContentSize.height + self.verticalGroupSpacing + } + verticalGroupOrigin += self.containerInsets.bottom + self.contentSize = CGSize(width: width, height: verticalGroupOrigin) + } + + func frame(groupIndex: Int, itemIndex: Int) -> CGRect { + let groupLayout = self.itemGroupLayouts[groupIndex] + + let row = itemIndex / self.itemsPerRow + let column = itemIndex % self.itemsPerRow + + return CGRect( + origin: CGPoint( + x: self.containerInsets.left + CGFloat(column) * (self.itemSize + self.horizontalSpacing), + y: groupLayout.frame.minY + groupLayout.itemTopOffset + CGFloat(row) * (self.itemSize + self.verticalSpacing) + ), + size: CGSize( + width: self.itemSize, + height: self.itemSize + ) + ) + } + + func visibleItems(for rect: CGRect) -> [(groupIndex: Int, groupItems: Range)] { + var result: [(groupIndex: Int, groupItems: Range)] = [] + + for groupIndex in 0 ..< self.itemGroupLayouts.count { + let group = self.itemGroupLayouts[groupIndex] + + if !rect.intersects(group.frame) { + continue + } + let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -group.frame.minY - group.itemTopOffset) + var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) + minVisibleRow = max(0, minVisibleRow) + let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) + + let minVisibleIndex = minVisibleRow * self.itemsPerRow + let maxVisibleIndex = min(group.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1) + + if maxVisibleIndex >= minVisibleIndex { + result.append(( + groupIndex: groupIndex, + groupItems: minVisibleIndex ..< (maxVisibleIndex + 1) + )) + } + } + + return result + } + } + + final class ItemLayer: MultiAnimationRenderTarget { + struct Key: Hashable { + var groupId: AnyHashable + var fileId: MediaId + } + + let item: Item + + private let file: TelegramMediaFile + private let placeholderColor: UIColor + private let size: CGSize + private var disposable: Disposable? + private var fetchDisposable: Disposable? + + private var isInHierarchyValue: Bool = false + public var isVisibleForAnimations: Bool = false { + didSet { + if self.isVisibleForAnimations != oldValue { + self.updatePlayback() + } + } + } + private var displayPlaceholder: Bool = false + + init( + item: Item, + context: AccountContext, + groupId: String, + attemptSynchronousLoad: Bool, + file: TelegramMediaFile, + cache: AnimationCache, + renderer: MultiAnimationRenderer, + placeholderColor: UIColor, + pointSize: CGSize + ) { + self.item = item + self.file = file + self.placeholderColor = placeholderColor + + let scale = min(2.0, UIScreenScale) + let pixelSize = CGSize(width: pointSize.width * scale, height: pointSize.height * scale) + self.size = CGSize(width: pixelSize.width / scale, height: pixelSize.height / scale) + + super.init() + + if file.isAnimatedSticker || file.isVideoSticker { + let loadAnimation: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.disposable = renderer.add(groupId: groupId, target: strongSelf, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in + let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) + + let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in + guard let result = result else { + return + } + + if file.isVideoSticker { + cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer) + } else { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { + writer.finish() + return + } + cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) + } + }) + + let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: file.resource).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() + } + }) + } + + if attemptSynchronousLoad { + if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { + self.displayPlaceholder = true + + if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { + self.contents = image.cgImage + } + } + + loadAnimation() + } else { + let _ = renderer.loadFirstFrame(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { _ in + loadAnimation() + }) + } + } else if let dimensions = file.dimensions { + let isSmall: Bool = false + self.disposable = (chatMessageSticker(account: context.account, file: file, small: isSmall, synchronousLoad: attemptSynchronousLoad)).start(next: { [weak self] resultTransform in + let boundingSize = CGSize(width: 93.0, height: 93.0) + let imageSize = dimensions.cgSize.aspectFilled(boundingSize) + + if let image = resultTransform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() { + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + + strongSelf.contents = image.cgImage + } + } + }) + } + } + + override public init(layer: Any) { + preconditionFailure() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable?.dispose() + self.fetchDisposable?.dispose() + } + + override public 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.shouldBeAnimating = shouldBePlaying + } + + override func updateDisplayPlaceholder(displayPlaceholder: Bool) { + if self.displayPlaceholder == displayPlaceholder { + return + } + + self.displayPlaceholder = displayPlaceholder + let file = self.file + let size = self.size + let placeholderColor = self.placeholderColor + + Queue.concurrentDefaultQueue().async { [weak self] in + if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + + if strongSelf.displayPlaceholder { + strongSelf.contents = image.cgImage + } + } + } + } + } + } + + private let scrollView: UIScrollView + + private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:] + private var visibleGroupHeaders: [AnyHashable: ComponentHostView] = [:] + private var ignoreScrolling: Bool = false + + private var component: EmojiPagerContentComponent? + private var pagerEnvironment: PagerComponentChildEnvironment? + private var theme: PresentationTheme? + private var itemLayout: ItemLayout? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let component = self.component, let (item, itemKey) = self.item(atPoint: recognizer.location(in: self)), let itemLayer = self.visibleItemLayers[itemKey] { + component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer) + } + } + } + + private func item(atPoint point: CGPoint) -> (Item, ItemLayer.Key)? { + let localPoint = self.convert(point, to: self.scrollView) + + for (key, itemLayer) in self.visibleItemLayers { + if itemLayer.frame.contains(localPoint) { + return (itemLayer.item, key) + } + } + + return nil + } + + private var previousScrollingOffset: CGFloat? + + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + if let presentation = scrollView.layer.presentation() { + scrollView.bounds = presentation.bounds + scrollView.layer.removeAllAnimations() + } + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + + self.updateVisibleItems(attemptSynchronousLoads: false) + + self.updateScrollingOffset(transition: .immediate) + } + + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + if velocity.y != 0.0 { + targetContentOffset.pointee.y = self.snappedContentOffset(proposedOffset: targetContentOffset.pointee.y) + } + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + self.snapScrollingOffsetToInsets() + } + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.snapScrollingOffsetToInsets() + } + + private func updateScrollingOffset(transition: Transition) { + if let previousScrollingOffsetValue = self.previousScrollingOffset { + let currentBounds = scrollView.bounds + let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) + let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) + let offsetToClosestEdge = min(offsetToTopEdge, offsetToBottomEdge) + + let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue + self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate( + relativeOffset: relativeOffset, + absoluteOffsetToClosestEdge: offsetToClosestEdge, + transition: transition + )) + self.previousScrollingOffset = scrollView.contentOffset.y + } + self.previousScrollingOffset = scrollView.contentOffset.y + } + + private func snappedContentOffset(proposedOffset: CGFloat) -> CGFloat { + guard let pagerEnvironment = self.pagerEnvironment else { + return proposedOffset + } + + var proposedOffset = proposedOffset + let bounds = self.bounds + if proposedOffset + bounds.height > self.scrollView.contentSize.height - pagerEnvironment.containerInsets.bottom { + proposedOffset = self.scrollView.contentSize.height - bounds.height + } + if proposedOffset < pagerEnvironment.containerInsets.top { + proposedOffset = 0.0 + } + + return proposedOffset + } + + private func snapScrollingOffsetToInsets() { + let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + + var currentBounds = self.scrollView.bounds + currentBounds.origin.y = self.snappedContentOffset(proposedOffset: currentBounds.minY) + transition.setBounds(view: self.scrollView, bounds: currentBounds) + + self.updateScrollingOffset(transition: transition) + } + + private func updateVisibleItems(attemptSynchronousLoads: Bool) { + guard let component = self.component, let theme = self.theme, let itemLayout = self.itemLayout else { + return + } + + var validIds = Set() + var validGroupHeaderIds = Set() + + for groupItems in itemLayout.visibleItems(for: self.scrollView.bounds) { + let itemGroup = component.itemGroups[groupItems.groupIndex] + let itemGroupLayout = itemLayout.itemGroupLayouts[groupItems.groupIndex] + + if let title = itemGroup.title { + validGroupHeaderIds.insert(itemGroup.id) + let groupHeaderView: ComponentHostView + if let current = self.visibleGroupHeaders[itemGroup.id] { + groupHeaderView = current + } else { + groupHeaderView = ComponentHostView() + self.visibleGroupHeaders[itemGroup.id] = groupHeaderView + self.scrollView.addSubview(groupHeaderView) + } + let groupHeaderSize = groupHeaderView.update( + transition: .immediate, + component: AnyComponent(Text( + text: title, font: Font.medium(12.0), color: theme.chat.inputMediaPanel.stickersSectionTextColor + )), + environment: {}, + containerSize: CGSize(width: itemLayout.contentSize.width - itemLayout.containerInsets.left - itemLayout.containerInsets.right, height: 100.0) + ) + groupHeaderView.frame = CGRect(origin: CGPoint(x: itemLayout.containerInsets.left, y: itemGroupLayout.frame.minY + 1.0), size: groupHeaderSize) + } + + for index in groupItems.groupItems.lowerBound ..< groupItems.groupItems.upperBound { + let item = itemGroup.items[index] + let itemId = ItemLayer.Key(groupId: itemGroup.id, fileId: item.file.fileId) + validIds.insert(itemId) + + let itemLayer: ItemLayer + if let current = self.visibleItemLayers[itemId] { + itemLayer = current + } else { + itemLayer = ItemLayer(item: item, context: component.context, groupId: "keyboard", attemptSynchronousLoad: attemptSynchronousLoads, file: item.file, cache: component.animationCache, renderer: component.animationRenderer, placeholderColor: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1), pointSize: CGSize(width: itemLayout.itemSize, height: itemLayout.itemSize)) + self.scrollView.layer.addSublayer(itemLayer) + self.visibleItemLayers[itemId] = itemLayer + } + + itemLayer.frame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: index) + itemLayer.isVisibleForAnimations = true + } + } + + var removedIds: [ItemLayer.Key] = [] + for (id, itemLayer) in self.visibleItemLayers { + if !validIds.contains(id) { + removedIds.append(id) + itemLayer.removeFromSuperlayer() + } + } + for id in removedIds { + self.visibleItemLayers.removeValue(forKey: id) + } + + var removedGroupHeaderIds: [AnyHashable] = [] + for (id, groupHeaderView) in self.visibleGroupHeaders { + if !validGroupHeaderIds.contains(id) { + removedGroupHeaderIds.append(id) + groupHeaderView.removeFromSuperview() + } + } + for id in removedGroupHeaderIds { + self.visibleGroupHeaders.removeValue(forKey: id) + } + } + + func update(component: EmojiPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.theme = environment[EntityKeyboardChildEnvironment.self].value.theme + + let pagerEnvironment = environment[PagerComponentChildEnvironment.self].value + self.pagerEnvironment = pagerEnvironment + + var itemGroups: [ItemGroupDescription] = [] + for itemGroup in component.itemGroups { + itemGroups.append(ItemGroupDescription( + hasTitle: itemGroup.title != nil, + itemCount: itemGroup.items.count + )) + } + + let itemLayout = ItemLayout(width: availableSize.width, containerInsets: UIEdgeInsets(top: pagerEnvironment.containerInsets.top + 9.0, left: pagerEnvironment.containerInsets.left + 12.0, bottom: 9.0 + pagerEnvironment.containerInsets.bottom, right: pagerEnvironment.containerInsets.right + 12.0), itemGroups: itemGroups, itemLayoutType: component.itemLayoutType) + self.itemLayout = itemLayout + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize)) + if self.scrollView.contentSize != itemLayout.contentSize { + self.scrollView.contentSize = itemLayout.contentSize + } + if self.scrollView.scrollIndicatorInsets != pagerEnvironment.containerInsets { + self.scrollView.scrollIndicatorInsets = pagerEnvironment.containerInsets + } + self.previousScrollingOffset = self.scrollView.contentOffset.y + self.ignoreScrolling = false + + self.updateVisibleItems(attemptSynchronousLoads: true) + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift new file mode 100644 index 0000000000..3bc7213fdd --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -0,0 +1,274 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox +import BlurredBackgroundComponent +import BundleIconComponent + +public final class EntityKeyboardChildEnvironment: Equatable { + public let theme: PresentationTheme + + public init(theme: PresentationTheme) { + self.theme = theme + } + + public static func ==(lhs: EntityKeyboardChildEnvironment, rhs: EntityKeyboardChildEnvironment) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + + return true + } +} + +public final class EntityKeyboardComponent: Component { + public let theme: PresentationTheme + public let bottomInset: CGFloat + public let emojiContent: EmojiPagerContentComponent + public let stickerContent: EmojiPagerContentComponent + public let gifContent: GifPagerContentComponent + public let externalTopPanelContainer: UIView? + public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void + + public init( + theme: PresentationTheme, + bottomInset: CGFloat, + emojiContent: EmojiPagerContentComponent, + stickerContent: EmojiPagerContentComponent, + gifContent: GifPagerContentComponent, + externalTopPanelContainer: UIView?, + topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void + ) { + self.theme = theme + self.bottomInset = bottomInset + self.emojiContent = emojiContent + self.stickerContent = stickerContent + self.gifContent = gifContent + self.externalTopPanelContainer = externalTopPanelContainer + self.topPanelExtensionUpdated = topPanelExtensionUpdated + } + + public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } + if lhs.emojiContent != rhs.emojiContent { + return false + } + if lhs.stickerContent != rhs.stickerContent { + return false + } + if lhs.gifContent != rhs.gifContent { + return false + } + if lhs.externalTopPanelContainer != rhs.externalTopPanelContainer { + return false + } + + return true + } + + public final class View: UIView { + private let pagerView: ComponentHostView + + private var component: EntityKeyboardComponent? + + override init(frame: CGRect) { + self.pagerView = ComponentHostView() + + super.init(frame: frame) + + self.clipsToBounds = true + + self.addSubview(self.pagerView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EntityKeyboardComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + var contents: [AnyComponentWithIdentity<(EntityKeyboardChildEnvironment, PagerComponentChildEnvironment)>] = [] + var contentTopPanels: [AnyComponentWithIdentity] = [] + var contentIcons: [AnyComponentWithIdentity] = [] + var contentAccessoryRightButtons: [AnyComponentWithIdentity] = [] + + contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(component.gifContent))) + var topGifItems: [EntityKeyboardTopPanelComponent.Item] = [] + topGifItems.append(EntityKeyboardTopPanelComponent.Item( + id: "recent", + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/RecentTabIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: CGSize(width: 30.0, height: 30.0)) + ) + )) + topGifItems.append(EntityKeyboardTopPanelComponent.Item( + id: "trending", + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/TrendingGifs", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: CGSize(width: 30.0, height: 30.0)) + ) + )) + contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent( + theme: component.theme, + items: topGifItems + )))) + contentIcons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputGifsIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )))) + /*contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputSettingsIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { + } + ).minSize(CGSize(width: 38.0, height: 38.0)))))*/ + + var topStickerItems: [EntityKeyboardTopPanelComponent.Item] = [] + for itemGroup in component.stickerContent.itemGroups { + if let id = itemGroup.id.base as? String { + let iconMapping: [String: String] = [ + "recent": "Chat/Input/Media/RecentTabIcon", + "premium": "Chat/Input/Media/PremiumIcon" + ] + if let iconName = iconMapping[id] { + topStickerItems.append(EntityKeyboardTopPanelComponent.Item( + id: id, + content: AnyComponent(BundleIconComponent( + name: iconName, + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: CGSize(width: 30.0, height: 30.0)) + ) + )) + } + } else { + if !itemGroup.items.isEmpty { + topStickerItems.append(EntityKeyboardTopPanelComponent.Item( + id: AnyHashable(itemGroup.items[0].file.fileId), + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: component.stickerContent.context, + file: itemGroup.items[0].file, + animationCache: component.stickerContent.animationCache, + animationRenderer: component.stickerContent.animationRenderer + )) + )) + } + } + } + contents.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(component.stickerContent))) + contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( + theme: component.theme, + items: topStickerItems + )))) + contentIcons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputStickersIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )))) + contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputSettingsIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { + component.stickerContent.inputInteraction.openStickerSettings() + } + ).minSize(CGSize(width: 38.0, height: 38.0))))) + + contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(component.emojiContent))) + var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] + for itemGroup in component.emojiContent.itemGroups { + if !itemGroup.items.isEmpty { + topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( + id: AnyHashable(itemGroup.items[0].file.fileId), + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: component.emojiContent.context, + file: itemGroup.items[0].file, + animationCache: component.emojiContent.animationCache, + animationRenderer: component.emojiContent.animationRenderer + )) + )) + } + } + contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( + theme: component.theme, + items: topEmojiItems + )))) + contentIcons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputEmojiIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )))) + contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputClearIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { + component.emojiContent.inputInteraction.deleteBackwards() + } + ).minSize(CGSize(width: 38.0, height: 38.0))))) + + let pagerSize = self.pagerView.update( + transition: transition, + component: AnyComponent(PagerComponent( + contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), + contents: contents, + contentTopPanels: contentTopPanels, + contentIcons: contentIcons, + contentAccessoryRightButtons: contentAccessoryRightButtons, + defaultId: "emoji", + contentBackground: AnyComponent(BlurredBackgroundComponent( + color: component.theme.chat.inputMediaPanel.stickersBackgroundColor.withMultipliedAlpha(0.75) + )), + topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent( + theme: component.theme + )), + externalTopPanelContainer: component.externalTopPanelContainer, + bottomPanel: AnyComponent(EntityKeyboardBottomPanelComponent( + theme: component.theme, + bottomInset: component.bottomInset, + deleteBackwards: { [weak self] in + self?.component?.emojiContent.inputInteraction.deleteBackwards() + } + )), + panelStateUpdated: { panelState, transition in + component.topPanelExtensionUpdated(panelState.topPanelHeight, transition) + } + )), + environment: { + EntityKeyboardChildEnvironment(theme: component.theme) + }, + containerSize: availableSize + ) + transition.setFrame(view: self.pagerView, frame: CGRect(origin: CGPoint(), size: pagerSize)) + + self.component = component + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift new file mode 100644 index 0000000000..16c2a40147 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift @@ -0,0 +1,310 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox +import ComponentDisplayAdapters +import BundleIconComponent + +private final class BottomPanelIconComponent: Component { + let content: AnyComponent + let action: () -> Void + + init( + content: AnyComponent, + action: @escaping () -> Void + ) { + self.content = content + self.action = action + } + + static func ==(lhs: BottomPanelIconComponent, rhs: BottomPanelIconComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + + return true + } + + final class View: UIView { + let contentView: ComponentHostView + + var component: BottomPanelIconComponent? + + override init(frame: CGRect) { + self.contentView = ComponentHostView() + self.contentView.isUserInteractionEnabled = false + + super.init(frame: frame) + + self.addSubview(self.contentView) + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.component?.action() + } + } + + func update(component: BottomPanelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let size = CGSize(width: 32.0, height: 32.0) + + let contentSize = self.contentView.update( + transition: transition, + component: component.content, + environment: {}, + containerSize: size + ) + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: (size.height - contentSize.height) / 2.0), size: contentSize)) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class EntityKeyboardBottomPanelComponent: Component { + typealias EnvironmentType = PagerComponentPanelEnvironment + + let theme: PresentationTheme + let bottomInset: CGFloat + let deleteBackwards: () -> Void + + init( + theme: PresentationTheme, + bottomInset: CGFloat, + deleteBackwards: @escaping () -> Void + ) { + self.theme = theme + self.bottomInset = bottomInset + self.deleteBackwards = deleteBackwards + } + + static func ==(lhs: EntityKeyboardBottomPanelComponent, rhs: EntityKeyboardBottomPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } + + return true + } + + final class View: UIView { + private final class AccessoryButtonView { + let id: AnyHashable + let view: ComponentHostView + + init(id: AnyHashable, view: ComponentHostView) { + self.id = id + self.view = view + } + } + + private let backgroundView: BlurredBackgroundView + private let separatorView: UIView + private var rightAccessoryButton: AccessoryButtonView? + + private var iconViews: [AnyHashable: ComponentHostView] = [:] + private var highlightedIconBackgroundView: UIView + + private var component: EntityKeyboardBottomPanelComponent? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + + self.separatorView = UIView() + self.separatorView.isUserInteractionEnabled = false + + self.highlightedIconBackgroundView = UIView() + self.highlightedIconBackgroundView.isUserInteractionEnabled = false + self.highlightedIconBackgroundView.layer.cornerRadius = 10.0 + self.highlightedIconBackgroundView.clipsToBounds = true + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.addSubview(self.highlightedIconBackgroundView) + self.addSubview(self.separatorView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EntityKeyboardBottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.component?.theme !== component.theme { + self.separatorView.backgroundColor = component.theme.chat.inputMediaPanel.panelSeparatorColor + self.backgroundView.updateColor(color: component.theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(1.0), transition: .immediate) + self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor + } + + let intrinsicHeight: CGFloat = 38.0 + let height = intrinsicHeight + component.bottomInset + + let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value + let activeContentId = panelEnvironment.activeContentId + + var rightAccessoryButtonComponent: AnyComponentWithIdentity? + for contentAccessoryRightButton in panelEnvironment.contentAccessoryRightButtons { + if contentAccessoryRightButton.id == activeContentId { + rightAccessoryButtonComponent = contentAccessoryRightButton + break + } + } + let previousRightAccessoryButton = self.rightAccessoryButton + + if let rightAccessoryButtonComponent = rightAccessoryButtonComponent { + var rightAccessoryButtonTransition = transition + let rightAccessoryButton: AccessoryButtonView + if let current = self.rightAccessoryButton, current.id == rightAccessoryButtonComponent.id { + rightAccessoryButton = current + } else { + rightAccessoryButtonTransition = .immediate + rightAccessoryButton = AccessoryButtonView(id: rightAccessoryButtonComponent.id, view: ComponentHostView()) + self.rightAccessoryButton = rightAccessoryButton + self.addSubview(rightAccessoryButton.view) + } + + let rightAccessoryButtonSize = rightAccessoryButton.view.update( + transition: rightAccessoryButtonTransition, + component: rightAccessoryButtonComponent.component, + environment: {}, + containerSize: CGSize(width: .greatestFiniteMagnitude, height: intrinsicHeight) + ) + rightAccessoryButtonTransition.setFrame(view: rightAccessoryButton.view, frame: CGRect(origin: CGPoint(x: availableSize.width - 2.0 - rightAccessoryButtonSize.width, y: 2.0), size: rightAccessoryButtonSize)) + } else { + self.rightAccessoryButton = nil + } + + if previousRightAccessoryButton !== self.rightAccessoryButton?.view { + if case .none = transition.animation { + previousRightAccessoryButton?.view.removeFromSuperview() + } else { + if let previousRightAccessoryButton = previousRightAccessoryButton { + let previousRightAccessoryButtonView = previousRightAccessoryButton.view + previousRightAccessoryButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + previousRightAccessoryButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousRightAccessoryButtonView] _ in + previousRightAccessoryButtonView?.removeFromSuperview() + }) + } + + if let rightAccessoryButtonView = self.rightAccessoryButton?.view { + rightAccessoryButtonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + rightAccessoryButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + } + + var validIconIds: [AnyHashable] = [] + var iconInfos: [AnyHashable: (size: CGSize, transition: Transition)] = [:] + + var iconTotalSize = CGSize() + let iconSpacing: CGFloat = 22.0 + + let navigateToContentId = panelEnvironment.navigateToContentId + + for icon in panelEnvironment.contentIcons { + validIconIds.append(icon.id) + + var iconTransition = transition + let iconView: ComponentHostView + if let current = self.iconViews[icon.id] { + iconView = current + } else { + iconTransition = .immediate + iconView = ComponentHostView() + self.iconViews[icon.id] = iconView + self.addSubview(iconView) + } + + let iconSize = iconView.update( + transition: iconTransition, + component: AnyComponent(BottomPanelIconComponent( + content: icon.component, + action: { + navigateToContentId(icon.id) + } + )), + environment: {}, + containerSize: CGSize(width: 32.0, height: 32.0) + ) + + iconInfos[icon.id] = (size: iconSize, transition: iconTransition) + + if !iconTotalSize.width.isZero { + iconTotalSize.width += iconSpacing + } + iconTotalSize.width += iconSize.width + iconTotalSize.height = max(iconTotalSize.height, iconSize.height) + } + + var nextIconOrigin = CGPoint(x: floor((availableSize.width - iconTotalSize.width) / 2.0), y: floor((intrinsicHeight - iconTotalSize.height) / 2.0) + 2.0) + for icon in panelEnvironment.contentIcons { + guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else { + continue + } + + let iconFrame = CGRect(origin: nextIconOrigin, size: iconInfo.size) + iconInfo.transition.setFrame(view: iconView, frame: iconFrame, completion: nil) + + if let activeContentId = activeContentId, activeContentId == icon.id { + self.highlightedIconBackgroundView.isHidden = false + transition.setFrame(view: self.highlightedIconBackgroundView, frame: iconFrame) + } + + nextIconOrigin.x += iconInfo.size.width + iconSpacing + } + + if activeContentId == nil { + self.highlightedIconBackgroundView.isHidden = true + } + + var removedIconViewIds: [AnyHashable] = [] + for (id, iconView) in self.iconViews { + if !validIconIds.contains(id) { + removedIconViewIds.append(id) + iconView.removeFromSuperview() + } + } + for id in removedIconViewIds { + self.iconViews.removeValue(forKey: id) + } + + transition.setFrame(view: self.separatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height))) + self.backgroundView.update(size: CGSize(width: availableSize.width, height: height), transition: transition.containedViewLayoutTransition) + + self.component = component + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift new file mode 100644 index 0000000000..875bdd0156 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift @@ -0,0 +1,136 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox + +final class EntityKeyboardTopContainerPanelComponent: Component { + typealias EnvironmentType = PagerComponentPanelEnvironment + + let theme: PresentationTheme + + init( + theme: PresentationTheme + ) { + self.theme = theme + } + + static func ==(lhs: EntityKeyboardTopContainerPanelComponent, rhs: EntityKeyboardTopContainerPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + + return true + } + + final class View: UIView { + private var panelViews: [AnyHashable: ComponentHostView] = [:] + + private var component: EntityKeyboardTopContainerPanelComponent? + private var panelEnvironment: PagerComponentPanelEnvironment? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EntityKeyboardTopContainerPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let intrinsicHeight: CGFloat = 41.0 + let height = intrinsicHeight + + let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value + + var transitionOffsetFraction: CGFloat = 0.0 + if case .none = transition.animation { + } else if let previousPanelEnvironment = self.panelEnvironment, let previousActiveContentId = previousPanelEnvironment.activeContentId, let activeContentId = panelEnvironment.activeContentId, previousActiveContentId != activeContentId { + if let previousIndex = panelEnvironment.contentTopPanels.firstIndex(where: { $0.id == previousActiveContentId }), let index = panelEnvironment.contentTopPanels.firstIndex(where: { $0.id == activeContentId }), previousIndex != index { + if index < previousIndex { + transitionOffsetFraction = -1.0 + } else { + transitionOffsetFraction = 1.0 + } + } + } + + self.component = component + self.panelEnvironment = panelEnvironment + self.state = state + + var validPanelIds = Set() + let visibleBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: intrinsicHeight)) + if let centralId = panelEnvironment.activeContentId, let centralIndex = panelEnvironment.contentTopPanels.firstIndex(where: { $0.id == centralId }) { + for index in 0 ..< panelEnvironment.contentTopPanels.count { + let panel = panelEnvironment.contentTopPanels[index] + let indexOffset = index - centralIndex + + let panelFrame = CGRect(origin: CGPoint(x: CGFloat(indexOffset) * availableSize.width, y: 0.0), size: CGSize(width: availableSize.width, height: intrinsicHeight)) + + let isInBounds = visibleBounds.intersects(panelFrame) + let isPartOfTransition: Bool + if !transitionOffsetFraction.isZero && self.panelViews[panel.id] != nil { + isPartOfTransition = true + } else { + isPartOfTransition = false + } + + if isInBounds || isPartOfTransition { + validPanelIds.insert(panel.id) + + var panelTransition = transition + let panelView: ComponentHostView + if let current = self.panelViews[panel.id] { + panelView = current + } else { + panelTransition = .immediate + panelView = ComponentHostView() + self.panelViews[panel.id] = panelView + self.addSubview(panelView) + } + + let _ = panelView.update( + transition: panelTransition, + component: panel.component, + environment: {}, + containerSize: panelFrame.size + ) + if isInBounds { + transition.animatePosition(view: panelView, from: CGPoint(x: transitionOffsetFraction * availableSize.width, y: 0.0), to: CGPoint(), additive: true, completion: nil) + } + panelTransition.setFrame(view: panelView, frame: panelFrame, completion: { [weak self] completed in + if isPartOfTransition && completed { + self?.state?.updated(transition: .immediate) + } + }) + } + } + } + var removedPanelIds: [AnyHashable] = [] + for (id, panelView) in self.panelViews { + if !validPanelIds.contains(id) { + removedPanelIds.append(id) + panelView.removeFromSuperview() + } + } + for id in removedPanelIds { + self.panelViews.removeValue(forKey: id) + } + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift new file mode 100644 index 0000000000..1a3630a28c --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -0,0 +1,315 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox +import AnimationCache +import MultiAnimationRenderer +import AccountContext + +final class EntityKeyboardAnimationTopPanelComponent: Component { + typealias EnvironmentType = Empty + + let context: AccountContext + let file: TelegramMediaFile + let animationCache: AnimationCache + let animationRenderer: MultiAnimationRenderer + + init( + context: AccountContext, + file: TelegramMediaFile, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer + ) { + self.context = context + self.file = file + self.animationCache = animationCache + self.animationRenderer = animationRenderer + } + + static func ==(lhs: EntityKeyboardAnimationTopPanelComponent, rhs: EntityKeyboardAnimationTopPanelComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.file.fileId != rhs.file.fileId { + return false + } + if lhs.animationCache !== rhs.animationCache { + return false + } + if lhs.animationRenderer !== rhs.animationRenderer { + return false + } + + return true + } + + final class View: UIView { + var itemLayer: EmojiPagerContentComponent.View.ItemLayer? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EntityKeyboardAnimationTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.itemLayer == nil { + let itemLayer = EmojiPagerContentComponent.View.ItemLayer( + item: EmojiPagerContentComponent.Item( + emoji: "", + file: component.file + ), + context: component.context, + groupId: "topPanel", + attemptSynchronousLoad: false, + file: component.file, + cache: component.animationCache, + renderer: component.animationRenderer, + placeholderColor: .lightGray, + pointSize: CGSize(width: 28.0, height: 28.0) + ) + self.itemLayer = itemLayer + self.layer.addSublayer(itemLayer) + itemLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0)) + itemLayer.isVisibleForAnimations = true + } + + return CGSize(width: 28.0, height: 28.0) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class EntityKeyboardTopPanelComponent: Component { + typealias EnvironmentType = Empty + + final class Item: Equatable { + let id: AnyHashable + let content: AnyComponent + + init(id: AnyHashable, content: AnyComponent) { + self.id = id + self.content = content + } + + static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.content != rhs.content { + return false + } + + return true + } + } + + let theme: PresentationTheme + let items: [Item] + + init( + theme: PresentationTheme, + items: [Item] + ) { + self.theme = theme + self.items = items + } + + static func ==(lhs: EntityKeyboardTopPanelComponent, rhs: EntityKeyboardTopPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.items != rhs.items { + return false + } + + return true + } + + final class View: UIView, UIScrollViewDelegate { + private struct ItemLayout { + let sideInset: CGFloat = 7.0 + let itemSize: CGFloat = 32.0 + let innerItemSize: CGFloat = 28.0 + let itemSpacing: CGFloat = 15.0 + let itemCount: Int + let contentSize: CGSize + + init(itemCount: Int) { + self.itemCount = itemCount + self.contentSize = CGSize(width: sideInset * 2.0 + CGFloat(itemCount) * self.itemSize + CGFloat(max(0, itemCount - 1)) * itemSpacing, height: 41.0) + } + + func containerFrame(at index: Int) -> CGRect { + return CGRect(origin: CGPoint(x: sideInset + CGFloat(index) * (self.itemSize + self.itemSpacing), y: floor((self.contentSize.height - self.itemSize) / 2.0)), size: CGSize(width: self.itemSize, height: self.itemSize)) + } + + func contentFrame(at index: Int) -> CGRect { + var frame = self.containerFrame(at: index) + frame.origin.x += floor((self.itemSize - self.innerItemSize)) / 2.0 + frame.origin.y += floor((self.itemSize - self.innerItemSize)) / 2.0 + frame.size = CGSize(width: self.innerItemSize, height: self.innerItemSize) + return frame + } + + func visibleItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) { + let offsetRect = rect.offsetBy(dx: -self.sideInset, dy: 0.0) + var minVisibleColumn = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize + self.itemSpacing))) + minVisibleColumn = max(0, minVisibleColumn) + let maxVisibleColumn = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize + self.itemSpacing))) + + let minVisibleIndex = minVisibleColumn + let maxVisibleIndex = min(maxVisibleColumn, self.itemCount - 1) + + return (minVisibleIndex, maxVisibleIndex) + } + } + + private let scrollView: UIScrollView + private var itemViews: [AnyHashable: ComponentHostView] = [:] + private var highlightedIconBackgroundView: UIView + + private var itemLayout: ItemLayout? + private var ignoreScrolling: Bool = false + + private var component: EntityKeyboardTopPanelComponent? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + + self.highlightedIconBackgroundView = UIView() + self.highlightedIconBackgroundView.isUserInteractionEnabled = false + self.highlightedIconBackgroundView.layer.cornerRadius = 10.0 + self.highlightedIconBackgroundView.clipsToBounds = true + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.addSubview(self.highlightedIconBackgroundView) + + self.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let strongSelf = self else { + return false + } + return strongSelf.scrollView.contentOffset.x > 0.0 + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + + self.updateVisibleItems(attemptSynchronousLoads: false) + } + + private func updateVisibleItems(attemptSynchronousLoads: Bool) { + guard let component = self.component, let itemLayout = self.itemLayout else { + return + } + + var validIds = Set() + let visibleItemRange = itemLayout.visibleItemRange(for: self.scrollView.bounds) + if !component.items.isEmpty && visibleItemRange.maxIndex >= visibleItemRange.minIndex { + for index in visibleItemRange.minIndex ... visibleItemRange.maxIndex { + let item = component.items[index] + validIds.insert(item.id) + + let itemView: ComponentHostView + if let current = self.itemViews[item.id] { + itemView = current + } else { + itemView = ComponentHostView() + self.scrollView.addSubview(itemView) + self.itemViews[item.id] = itemView + } + + let itemOuterFrame = itemLayout.contentFrame(at: index) + let itemSize = itemView.update( + transition: .immediate, + component: item.content, + environment: {}, + containerSize: itemOuterFrame.size + ) + itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) + } + } + var removedIds: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removedIds.append(id) + itemView.removeFromSuperview() + } + } + for id in removedIds { + self.itemViews.removeValue(forKey: id) + } + } + + func update(component: EntityKeyboardTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.component?.theme !== component.theme { + self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor + } + self.component = component + + let intrinsicHeight: CGFloat = 41.0 + let height = intrinsicHeight + + let itemLayout = ItemLayout(itemCount: component.items.count) + self.itemLayout = itemLayout + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: intrinsicHeight))) + if self.scrollView.contentSize != itemLayout.contentSize { + self.scrollView.contentSize = itemLayout.contentSize + } + self.ignoreScrolling = false + + if let _ = component.items.first { + self.highlightedIconBackgroundView.isHidden = false + let itemFrame = itemLayout.containerFrame(at: 0) + transition.setFrame(view: self.highlightedIconBackgroundView, frame: itemFrame) + } + + self.updateVisibleItems(attemptSynchronousLoads: true) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift new file mode 100644 index 0000000000..3b8a5c5c17 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -0,0 +1,585 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox +import MultiAnimationRenderer +import AnimationCache +import AccountContext +import LottieAnimationCache +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import SwiftSignalKit +import ShimmerEffect +import PagerComponent +import SoftwareVideo +import AVFoundation +import PhotoResources + +private class GifVideoLayer: AVSampleBufferDisplayLayer { + private let context: AccountContext + private let file: TelegramMediaFile + + private var frameManager: SoftwareVideoLayerFrameManager? + + private var thumbnailDisposable: Disposable? + + private var playbackTimestamp: Double = 0.0 + private var playbackTimer: SwiftSignalKit.Timer? + + var started: (() -> Void)? + + var shouldBeAnimating: Bool = false { + didSet { + if self.shouldBeAnimating == oldValue { + return + } + + if self.shouldBeAnimating { + self.playbackTimer?.invalidate() + self.playbackTimer = SwiftSignalKit.Timer(timeout: 1.0 / 30.0, repeat: true, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.frameManager?.tick(timestamp: strongSelf.playbackTimestamp) + strongSelf.playbackTimestamp += 1.0 / 30.0 + }, queue: .mainQueue()) + self.playbackTimer?.start() + } else { + self.playbackTimer?.invalidate() + self.playbackTimer = nil + } + } + } + + init(context: AccountContext, file: TelegramMediaFile, synchronousLoad: Bool) { + self.context = context + self.file = file + + super.init() + + self.videoGravity = .resizeAspectFill + + if let dimensions = file.dimensions { + self.thumbnailDisposable = (mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .savedGif(media: self.file), synchronousLoad: synchronousLoad, nilForEmptyResult: true) + |> deliverOnMainQueue).start(next: { [weak self] transform in + guard let strongSelf = self else { + return + } + let boundingSize = CGSize(width: 93.0, height: 93.0) + let imageSize = dimensions.cgSize.aspectFilled(boundingSize) + + if let image = transform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() { + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.contents = image.cgImage + strongSelf.setupVideo() + strongSelf.started?() + } + } + } else { + strongSelf.setupVideo() + } + }) + } else { + self.setupVideo() + } + } + + override init(layer: Any) { + preconditionFailure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.thumbnailDisposable?.dispose() + } + + private func setupVideo() { + let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: .savedGif(media: self.file), layerHolder: nil, layer: self) + self.frameManager = frameManager + frameManager.started = { [weak self] in + guard let strongSelf = self else { + return + } + let _ = strongSelf + } + frameManager.start() + } +} + +public final class GifPagerContentComponent: Component { + public typealias EnvironmentType = (EntityKeyboardChildEnvironment, PagerComponentChildEnvironment) + + public final class InputInteraction { + public let performItemAction: (Item, UIView, CGRect) -> Void + + public init( + performItemAction: @escaping (Item, UIView, CGRect) -> Void + ) { + self.performItemAction = performItemAction + } + } + + public final class Item: Equatable { + public let file: TelegramMediaFile + + public init(file: TelegramMediaFile) { + self.file = file + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if lhs.file.fileId != rhs.file.fileId { + return false + } + + return true + } + } + + public let context: AccountContext + public let inputInteraction: InputInteraction + public let items: [Item] + + public init( + context: AccountContext, + inputInteraction: InputInteraction, + items: [Item] + ) { + self.context = context + self.inputInteraction = inputInteraction + self.items = items + } + + public static func ==(lhs: GifPagerContentComponent, rhs: GifPagerContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.inputInteraction !== rhs.inputInteraction { + return false + } + if lhs.items != rhs.items { + return false + } + + return true + } + + public final class View: UIView, UIScrollViewDelegate { + private struct ItemGroupDescription: Equatable { + let hasTitle: Bool + let itemCount: Int + } + + private struct ItemGroupLayout: Equatable { + let frame: CGRect + let itemTopOffset: CGFloat + let itemCount: Int + } + + private struct ItemLayout: Equatable { + let width: CGFloat + let containerInsets: UIEdgeInsets + let itemCount: Int + let itemSize: CGFloat + let horizontalSpacing: CGFloat + let verticalSpacing: CGFloat + let itemsPerRow: Int + let contentSize: CGSize + + init(width: CGFloat, containerInsets: UIEdgeInsets, itemCount: Int) { + self.width = width + self.containerInsets = containerInsets + self.itemCount = itemCount + self.horizontalSpacing = 1.0 + self.verticalSpacing = 1.0 + + let itemHorizontalSpace = width - self.containerInsets.left - self.containerInsets.right + self.itemSize = floor((width - self.horizontalSpacing * 2.0) / 3.0) + + self.itemsPerRow = Int((itemHorizontalSpace + self.horizontalSpacing) / (self.itemSize + self.horizontalSpacing)) + let numRowsInGroup = (itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow + self.contentSize = CGSize(width: width, height: self.containerInsets.top + self.containerInsets.bottom + CGFloat(numRowsInGroup) * self.itemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) + } + + func frame(at index: Int) -> CGRect { + let row = index / self.itemsPerRow + let column = index % self.itemsPerRow + + var rect = CGRect( + origin: CGPoint( + x: self.containerInsets.left + CGFloat(column) * (self.itemSize + self.horizontalSpacing), + y: self.containerInsets.top + CGFloat(row) * (self.itemSize + self.verticalSpacing) + ), + size: CGSize( + width: self.itemSize, + height: self.itemSize + ) + ) + + if column == self.itemsPerRow - 1 { + rect.size.width = self.width - self.containerInsets.right - rect.minX + } + + return rect + } + + func visibleItems(for rect: CGRect) -> Range? { + let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -containerInsets.top) + var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) + minVisibleRow = max(0, minVisibleRow) + let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) + + let minVisibleIndex = minVisibleRow * self.itemsPerRow + let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1) + + if maxVisibleIndex >= minVisibleIndex { + return minVisibleIndex ..< (maxVisibleIndex + 1) + } else { + return nil + } + } + } + + fileprivate final class ItemLayer: GifVideoLayer { + let item: Item + + private let file: TelegramMediaFile + private let placeholderColor: UIColor + private var disposable: Disposable? + private var fetchDisposable: Disposable? + + private var isInHierarchyValue: Bool = false + public var isVisibleForAnimations: Bool = false { + didSet { + if self.isVisibleForAnimations != oldValue { + self.updatePlayback() + } + } + } + private var displayPlaceholder: Bool = false + + init( + item: Item, + context: AccountContext, + groupId: String, + attemptSynchronousLoad: Bool, + file: TelegramMediaFile, + placeholderColor: UIColor + ) { + self.item = item + self.file = file + self.placeholderColor = placeholderColor + + super.init(context: context, file: file, synchronousLoad: attemptSynchronousLoad) + + self.updateDisplayPlaceholder(displayPlaceholder: true) + + self.started = { [weak self] in + self?.updateDisplayPlaceholder(displayPlaceholder: false) + } + + /*if attemptSynchronousLoad { + if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { + self.displayPlaceholder = true + + if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { + self.contents = image.cgImage + } + } + } + + self.disposable = renderer.add(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in + let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) + + let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in + guard let result = result else { + return + } + + guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { + writer.finish() + return + } + cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) + }) + + let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() + } + })*/ + } + + required 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.shouldBeAnimating = shouldBePlaying + } + + func updateDisplayPlaceholder(displayPlaceholder: Bool) { + if self.displayPlaceholder == displayPlaceholder { + return + } + + self.displayPlaceholder = displayPlaceholder + + if displayPlaceholder { + let placeholderColor = self.placeholderColor + self.backgroundColor = placeholderColor.cgColor + } else { + self.backgroundColor = nil + } + } + } + + private let scrollView: UIScrollView + + private var visibleItemLayers: [MediaId: ItemLayer] = [:] + private var ignoreScrolling: Bool = false + + private var component: GifPagerContentComponent? + private var pagerEnvironment: PagerComponentChildEnvironment? + private var theme: PresentationTheme? + private var itemLayout: ItemLayout? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[item.file.fileId] { + component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemView.frame, to: self)) + } + } + } + + private func item(atPoint point: CGPoint) -> Item? { + let localPoint = self.convert(point, to: self.scrollView) + + for (_, itemLayer) in self.visibleItemLayers { + if itemLayer.frame.contains(localPoint) { + return itemLayer.item + } + } + + return nil + } + + private var previousScrollingOffset: CGFloat? + + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + if let presentation = scrollView.layer.presentation() { + scrollView.bounds = presentation.bounds + scrollView.layer.removeAllAnimations() + } + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + + self.updateVisibleItems(attemptSynchronousLoads: false) + + self.updateScrollingOffset(transition: .immediate) + } + + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + if velocity.y != 0.0 { + targetContentOffset.pointee.y = self.snappedContentOffset(proposedOffset: targetContentOffset.pointee.y) + } + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + self.snapScrollingOffsetToInsets() + } + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.snapScrollingOffsetToInsets() + } + + private func updateScrollingOffset(transition: Transition) { + if let previousScrollingOffsetValue = self.previousScrollingOffset { + let currentBounds = scrollView.bounds + let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) + let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) + let offsetToClosestEdge = min(offsetToTopEdge, offsetToBottomEdge) + + let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue + self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate( + relativeOffset: relativeOffset, + absoluteOffsetToClosestEdge: offsetToClosestEdge, + transition: transition + )) + self.previousScrollingOffset = scrollView.contentOffset.y + } + self.previousScrollingOffset = scrollView.contentOffset.y + } + + private func snappedContentOffset(proposedOffset: CGFloat) -> CGFloat { + guard let pagerEnvironment = self.pagerEnvironment else { + return proposedOffset + } + + var proposedOffset = proposedOffset + let bounds = self.bounds + if proposedOffset + bounds.height > self.scrollView.contentSize.height - pagerEnvironment.containerInsets.bottom { + proposedOffset = self.scrollView.contentSize.height - bounds.height + } + if proposedOffset < pagerEnvironment.containerInsets.top { + proposedOffset = 0.0 + } + + return proposedOffset + } + + private func snapScrollingOffsetToInsets() { + let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + + var currentBounds = self.scrollView.bounds + currentBounds.origin.y = self.snappedContentOffset(proposedOffset: currentBounds.minY) + transition.setBounds(view: self.scrollView, bounds: currentBounds) + + self.updateScrollingOffset(transition: transition) + } + + private func updateVisibleItems(attemptSynchronousLoads: Bool) { + guard let component = self.component, let theme = self.theme, let itemLayout = self.itemLayout else { + return + } + + var validIds = Set() + + if let itemRange = itemLayout.visibleItems(for: self.scrollView.bounds) { + for index in itemRange.lowerBound ..< itemRange.upperBound { + let item = component.items[index] + let itemId = item.file.fileId + validIds.insert(itemId) + + let itemLayer: ItemLayer + if let current = self.visibleItemLayers[itemId] { + itemLayer = current + } else { + itemLayer = ItemLayer( + item: item, + context: component.context, + groupId: "savedGif", + attemptSynchronousLoad: attemptSynchronousLoads, + file: item.file, + placeholderColor: theme.chat.inputMediaPanel.stickersBackgroundColor + ) + self.scrollView.layer.addSublayer(itemLayer) + self.visibleItemLayers[itemId] = itemLayer + } + + itemLayer.frame = itemLayout.frame(at: index) + itemLayer.isVisibleForAnimations = true + } + } + + var removedIds: [MediaId] = [] + for (id, itemLayer) in self.visibleItemLayers { + if !validIds.contains(id) { + removedIds.append(id) + itemLayer.removeFromSuperlayer() + } + } + for id in removedIds { + self.visibleItemLayers.removeValue(forKey: id) + } + } + + func update(component: GifPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.theme = environment[EntityKeyboardChildEnvironment.self].value.theme + + let pagerEnvironment = environment[PagerComponentChildEnvironment.self].value + self.pagerEnvironment = pagerEnvironment + + let itemLayout = ItemLayout( + width: availableSize.width, + containerInsets: UIEdgeInsets(top: pagerEnvironment.containerInsets.top, left: pagerEnvironment.containerInsets.left, bottom: pagerEnvironment.containerInsets.bottom, right: pagerEnvironment.containerInsets.right), + itemCount: component.items.count + ) + self.itemLayout = itemLayout + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize)) + if self.scrollView.contentSize != itemLayout.contentSize { + self.scrollView.contentSize = itemLayout.contentSize + } + if self.scrollView.scrollIndicatorInsets != pagerEnvironment.containerInsets { + self.scrollView.scrollIndicatorInsets = pagerEnvironment.containerInsets + } + self.previousScrollingOffset = self.scrollView.contentOffset.y + self.ignoreScrolling = false + + self.updateVisibleItems(attemptSynchronousLoads: true) + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + diff --git a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift index 1049ce1d21..a3f4e96a63 100644 --- a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift +++ b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift @@ -6,18 +6,23 @@ import RLottieBinding import GZip public func cacheLottieAnimation(data: Data, width: Int, height: Int, writer: AnimationCacheItemWriter) { - let decompressedData = TGGUnzipData(data, 512 * 1024) ?? data - guard let animation = LottieInstance(data: decompressedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else { + writer.queue.async { + let decompressedData = TGGUnzipData(data, 1 * 1024 * 1024) ?? data + guard let animation = LottieInstance(data: decompressedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else { + writer.finish() + return + } + + let frameDuration = 1.0 / Double(animation.frameRate) + for i in 0 ..< animation.frameCount { + if writer.isCancelled { + break + } + writer.add(with: { surface in + animation.renderFrame(with: i, into: surface.argb, width: Int32(surface.width), height: Int32(surface.height), bytesPerRow: Int32(surface.bytesPerRow)) + }, proposedWidth: width, proposedHeight: height, duration: frameDuration) + } + writer.finish() - return } - let size = CGSize(width: width, height: height) - let context = DrawingContext(size: size, scale: 1.0, opaque: false, clear: true) - let frameDuration = 1.0 / Double(animation.frameRate) - for i in 0 ..< animation.frameCount { - animation.renderFrame(with: i, into: context.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(context.scaledSize.width), height: Int32(context.scaledSize.height), bytesPerRow: Int32(context.bytesPerRow)) - writer.add(bytes: context.bytes, length: context.length, width: Int(context.scaledSize.width), height: Int(context.scaledSize.height), bytesPerRow: Int(context.bytesPerRow), duration: frameDuration) - } - - writer.finish() } diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index 97fb1cee2c..c952603feb 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -5,8 +5,9 @@ import Display import AnimationCache public protocol MultiAnimationRenderer: AnyObject { - func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable - func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String) -> Bool + func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable + func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool + func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable } open class MultiAnimationRenderTarget: SimpleLayer { @@ -28,6 +29,9 @@ open class MultiAnimationRenderTarget: SimpleLayer { f() } } + + open func updateDisplayPlaceholder(displayPlaceholder: Bool) { + } } private func convertFrameToImage(frame: AnimationCacheItemFrame) -> UIImage? { @@ -45,43 +49,19 @@ private func convertFrameToImage(frame: AnimationCacheItemFrame) -> UIImage? { private final class FrameGroup { let image: UIImage let size: CGSize - let frameRange: Range - let count: Int - let skip: Int + let timestamp: Double - init?(item: AnimationCacheItem, baseFrameIndex: Int, count: Int, skip: Int) { - if count == 0 { - return nil - } - - assert(count % skip == 0) - - let actualCount = count / skip - - guard let firstFrame = item.getFrame(index: baseFrameIndex % item.numFrames) else { + init?(item: AnimationCacheItem, timestamp: Double) { + guard let firstFrame = item.getFrame(at: timestamp) else { return nil } switch firstFrame.format { case let .rgba(width, height, bytesPerRow): - let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height * actualCount)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) - for i in stride(from: baseFrameIndex, to: baseFrameIndex + count, by: skip) { - let frame: AnimationCacheItemFrame - if i == baseFrameIndex { - frame = firstFrame - } else { - if let nextFrame = item.getFrame(index: i % item.numFrames) { - frame = nextFrame - } else { - return nil - } - } + let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) - let localFrameIndex = (i - baseFrameIndex) / skip - - frame.data.withUnsafeBytes { bytes -> Void in - memcpy(context.bytes.advanced(by: localFrameIndex * height * bytesPerRow), bytes.baseAddress!.advanced(by: frame.range.lowerBound), height * bytesPerRow) - } + firstFrame.data.withUnsafeBytes { bytes -> Void in + memcpy(context.bytes, bytes.baseAddress!.advanced(by: firstFrame.range.lowerBound), height * bytesPerRow) } guard let image = context.generateImage() else { @@ -90,22 +70,9 @@ private final class FrameGroup { self.image = image self.size = CGSize(width: CGFloat(width), height: CGFloat(height)) - self.frameRange = baseFrameIndex ..< (baseFrameIndex + count) - self.count = count - self.skip = skip + self.timestamp = timestamp } } - - func contentsRect(index: Int) -> CGRect? { - if !self.frameRange.contains(index) { - return nil - } - let actualCount = self.count / self.skip - let localIndex = (index - self.frameRange.lowerBound) / self.skip - - let itemHeight = 1.0 / CGFloat(actualCount) - return CGRect(origin: CGPoint(x: 0.0, y: CGFloat(localIndex) * itemHeight), size: CGSize(width: 1.0, height: itemHeight)) - } } private final class LoadFrameGroupTask { @@ -124,7 +91,7 @@ private final class ItemAnimationContext { private var disposable: Disposable? private var displayLink: ConstantDisplayLinkAnimator? - private var frameIndex: Int = 0 + private var timestamp: Double = 0.0 private var item: AnimationCacheItem? private var currentFrameGroup: FrameGroup? @@ -140,17 +107,25 @@ private final class ItemAnimationContext { let targets = Bag>() - init(cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) { + init(cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) { self.cache = cache self.stateUpdated = stateUpdated - self.disposable = cache.get(sourceId: itemId, fetch: fetch).start(next: { [weak self] item in + self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in Queue.mainQueue().async { - guard let strongSelf = self, let item = item else { + guard let strongSelf = self else { return } - strongSelf.item = item + strongSelf.item = result.item strongSelf.updateIsPlaying() + + if result.item == nil { + for target in strongSelf.targets.copyItems() { + if let target = target.value { + target.updateDisplayPlaceholder(displayPlaceholder: true) + } + } + } } }) } @@ -161,13 +136,9 @@ private final class ItemAnimationContext { } func updateAddedTarget(target: MultiAnimationRenderTarget) { - if let item = self.item, let currentFrameGroup = self.currentFrameGroup { - let currentFrame = self.frameIndex % item.numFrames - - if let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) { - target.contents = currentFrameGroup.image.cgImage - target.contentsRect = contentsRect - } + if let currentFrameGroup = self.currentFrameGroup { + target.updateDisplayPlaceholder(displayPlaceholder: false) + target.contents = currentFrameGroup.image.cgImage } self.updateIsPlaying() @@ -195,26 +166,26 @@ private final class ItemAnimationContext { self.isPlaying = isPlaying } - func animationTick() -> LoadFrameGroupTask? { - return self.update(advanceFrame: true) + func animationTick(advanceTimestamp: Double) -> LoadFrameGroupTask? { + return self.update(advanceTimestamp: advanceTimestamp) } - private func update(advanceFrame: Bool) -> LoadFrameGroupTask? { + private func update(advanceTimestamp: Double?) -> LoadFrameGroupTask? { guard let item = self.item else { return nil } - let currentFrame = self.frameIndex % item.numFrames + let timestamp = self.timestamp + if let advanceTimestamp = advanceTimestamp { + self.timestamp += advanceTimestamp + } - if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.frameRange.contains(currentFrame) { + if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.timestamp == self.timestamp { } else if !self.isLoadingFrameGroup { - self.currentFrameGroup = nil self.isLoadingFrameGroup = true return LoadFrameGroupTask(task: { [weak self] in - let possibleCounts: [Int] = [10, 12, 14, 16, 18, 20] - let countIndex = Int.random(in: 0 ..< possibleCounts.count) - let currentFrameGroup = FrameGroup(item: item, baseFrameIndex: currentFrame, count: possibleCounts[countIndex], skip: 2) + let currentFrameGroup = FrameGroup(item: item, timestamp: timestamp) return { guard let strongSelf = self else { @@ -226,22 +197,21 @@ private final class ItemAnimationContext { if let currentFrameGroup = currentFrameGroup { strongSelf.currentFrameGroup = currentFrameGroup for target in strongSelf.targets.copyItems() { - target.value?.contents = currentFrameGroup.image.cgImage + if let target = target.value { + target.contents = currentFrameGroup.image.cgImage + target.updateDisplayPlaceholder(displayPlaceholder: false) + } } - - let _ = strongSelf.update(advanceFrame: false) } } }) } - if advanceFrame { - self.frameIndex += 2 - } - - if let currentFrameGroup = self.currentFrameGroup, let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) { + if let _ = self.currentFrameGroup { for target in self.targets.copyItems() { - target.value?.contentsRect = contentsRect + if let target = target.value { + target.updateDisplayPlaceholder(displayPlaceholder: false) + } } } @@ -251,6 +221,7 @@ private final class ItemAnimationContext { public final class MultiAnimationRendererImpl: MultiAnimationRenderer { private final class GroupContext { + private let firstFrameQueue: Queue private let stateUpdated: () -> Void private var itemContexts: [String: ItemAnimationContext] = [:] @@ -263,16 +234,17 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } } - init(stateUpdated: @escaping () -> Void) { + init(firstFrameQueue: Queue, stateUpdated: @escaping () -> Void) { + self.firstFrameQueue = firstFrameQueue self.stateUpdated = stateUpdated } - func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable { + func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { let itemContext: ItemAnimationContext if let current = self.itemContexts[itemId] { itemContext = current } else { - itemContext = ItemAnimationContext(cache: cache, itemId: itemId, fetch: fetch, stateUpdated: { [weak self] in + itemContext = ItemAnimationContext(cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in guard let strongSelf = self else { return } @@ -318,9 +290,9 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } } - func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String) -> Bool { - if let item = cache.getSynchronously(sourceId: itemId) { - guard let frameGroup = FrameGroup(item: item, baseFrameIndex: 0, count: 1, skip: 1) else { + func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) { + guard let frameGroup = FrameGroup(item: item, timestamp: 0.0) else { return false } @@ -332,6 +304,33 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } } + func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable { + return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, completion: { [weak target] item in + guard let item = item else { + Queue.mainQueue().async { + completion(false) + } + return + } + + let frameGroup = FrameGroup(item: item, timestamp: 0.0) + + Queue.mainQueue().async { + guard let target = target else { + completion(false) + return + } + if let frameGroup = frameGroup { + target.contents = frameGroup.image.cgImage + + completion(true) + } else { + completion(false) + } + } + }) + } + private func updateIsPlaying() { var isPlaying = false for (_, itemContext) in self.itemContexts { @@ -344,11 +343,11 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { self.isPlaying = isPlaying } - func animationTick() -> [LoadFrameGroupTask] { + func animationTick(advanceTimestamp: Double) -> [LoadFrameGroupTask] { var tasks: [LoadFrameGroupTask] = [] for (_, itemContext) in self.itemContexts { if itemContext.isPlaying { - if let task = itemContext.animationTick() { + if let task = itemContext.animationTick(advanceTimestamp: advanceTimestamp) { tasks.append(task) } } @@ -358,7 +357,9 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } } + private let firstFrameQueue: Queue private var groupContexts: [String: GroupContext] = [:] + private var frameSkip: Int private var displayLink: ConstantDisplayLinkAnimator? private(set) var isPlaying: Bool = false { @@ -372,7 +373,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } strongSelf.animationTick() } - self.displayLink?.frameInterval = 2 + self.displayLink?.frameInterval = self.frameSkip self.displayLink?.isPaused = false } } else { @@ -386,14 +387,21 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } public init() { + self.firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive) + + if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.activeProcessorCount > 2 { + self.frameSkip = 1 + } else { + self.frameSkip = 2 + } } - public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable { + public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { let groupContext: GroupContext if let current = self.groupContexts[groupId] { groupContext = current } else { - groupContext = GroupContext(stateUpdated: { [weak self] in + groupContext = GroupContext(firstFrameQueue: self.firstFrameQueue, stateUpdated: { [weak self] in guard let strongSelf = self else { return } @@ -402,19 +410,19 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { self.groupContexts[groupId] = groupContext } - let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, fetch: fetch) + let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch) return ActionDisposable { disposable.dispose() } } - public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String) -> Bool { + public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { let groupContext: GroupContext if let current = self.groupContexts[groupId] { groupContext = current } else { - groupContext = GroupContext(stateUpdated: { [weak self] in + groupContext = GroupContext(firstFrameQueue: self.firstFrameQueue, stateUpdated: { [weak self] in guard let strongSelf = self else { return } @@ -423,7 +431,24 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { self.groupContexts[groupId] = groupContext } - return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId) + return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size) + } + + public func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContexts[groupId] { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: self.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContexts[groupId] = groupContext + } + + return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, completion: completion) } private func updateIsPlaying() { @@ -438,11 +463,20 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { self.isPlaying = isPlaying } + private var previousTimestamp: Double? + private func animationTick() { + let timestamp = CFAbsoluteTimeGetCurrent() + if let _ = self.previousTimestamp { + } + self.previousTimestamp = timestamp + + let secondsPerFrame = Double(self.frameSkip) / 60.0 + var tasks: [LoadFrameGroupTask] = [] for (_, groupContext) in self.groupContexts { if groupContext.isPlaying { - tasks.append(contentsOf: groupContext.animationTick()) + tasks.append(contentsOf: groupContext.animationTick(advanceTimestamp: secondsPerFrame)) } } diff --git a/submodules/TelegramUI/Components/MultiVideoRenderer/BUILD b/submodules/TelegramUI/Components/MultiVideoRenderer/BUILD new file mode 100644 index 0000000000..e6fa67cd5f --- /dev/null +++ b/submodules/TelegramUI/Components/MultiVideoRenderer/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MultiVideoRenderer", + module_name = "MultiVideoRenderer", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/SoftwareVideo:SoftwareVideo", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/MultiVideoRenderer/Sources/MultiVideoRenderer.swift b/submodules/TelegramUI/Components/MultiVideoRenderer/Sources/MultiVideoRenderer.swift new file mode 100644 index 0000000000..359224b564 --- /dev/null +++ b/submodules/TelegramUI/Components/MultiVideoRenderer/Sources/MultiVideoRenderer.swift @@ -0,0 +1,399 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import SoftwareVideo + +/*public protocol MultiVideoRenderer: AnyObject { + func add(groupId: String, target: MultiVideoRenderTarget, itemId: String, size: CGSize, source: @escaping (@escaping (String) -> Void) -> Disposable) -> Disposable +} + +open class MultiVideoRenderTarget: SimpleLayer { + fileprivate let deinitCallbacks = Bag<() -> Void>() + fileprivate let updateStateCallbacks = Bag<() -> Void>() + + public final var shouldBeAnimating: Bool = false { + didSet { + if self.shouldBeAnimating != oldValue { + for f in self.updateStateCallbacks.copyItems() { + f() + } + } + } + } + + deinit { + for f in self.deinitCallbacks.copyItems() { + f() + } + } + + open func updateDisplayPlaceholder(displayPlaceholder: Bool) { + } +} + +private final class ItemVideoContext { + static let queue = Queue(name: "ItemVideoContext", qos: .default) + + private let stateUpdated: () -> Void + + private var disposable: Disposable? + private var displayLink: ConstantDisplayLinkAnimator? + private var frameManager: SoftwareVideoLayerFrameManager? + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + self.stateUpdated() + } + } + } + + let targets = Bag>() + + init(itemId: String, source: @escaping (@escaping (String) -> Void) -> Disposable, stateUpdated: @escaping () -> Void) { + self.stateUpdated = stateUpdated + + self.disposable = source({ [weak self] in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + //strongSelf.frameManager = SoftwareVideoLayerFrameManager(account: <#T##Account#>, fileReference: <#T##FileMediaReference#>, layerHolder: <#T##SampleBufferLayer#>) + strongSelf.updateIsPlaying() + + if result.item == nil { + for target in strongSelf.targets.copyItems() { + if let target = target.value { + target.updateDisplayPlaceholder(displayPlaceholder: true) + } + } + } + } + }) + } + + deinit { + self.disposable?.dispose() + self.displayLink?.invalidate() + } + + func updateAddedTarget(target: MultiAnimationRenderTarget) { + if let item = self.item, let currentFrameGroup = self.currentFrameGroup { + let currentFrame = self.frameIndex % item.numFrames + + if let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) { + target.updateDisplayPlaceholder(displayPlaceholder: false) + target.contents = currentFrameGroup.image.cgImage + target.contentsRect = contentsRect + } + } + + self.updateIsPlaying() + } + + func updateIsPlaying() { + var isPlaying = true + if self.item == nil { + isPlaying = false + } + + var shouldBeAnimating = false + for target in self.targets.copyItems() { + if let target = target.value { + if target.shouldBeAnimating { + shouldBeAnimating = true + break + } + } + } + if !shouldBeAnimating { + isPlaying = false + } + + self.isPlaying = isPlaying + } + + func animationTick() -> LoadFrameGroupTask? { + return self.update(advanceFrame: true) + } + + private func update(advanceFrame: Bool) -> LoadFrameGroupTask? { + guard let item = self.item else { + return nil + } + + let currentFrame = self.frameIndex % item.numFrames + + if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.frameRange.contains(currentFrame) { + } else if !self.isLoadingFrameGroup { + self.currentFrameGroup = nil + self.isLoadingFrameGroup = true + let frameSkip = self.frameSkip + + return LoadFrameGroupTask(task: { [weak self] in + let possibleCounts: [Int] = [10, 12, 14, 16, 18, 20] + let countIndex = Int.random(in: 0 ..< possibleCounts.count) + let currentFrameGroup = FrameGroup(item: item, baseFrameIndex: currentFrame, count: possibleCounts[countIndex], skip: frameSkip) + + return { + guard let strongSelf = self else { + return + } + + strongSelf.isLoadingFrameGroup = false + + if let currentFrameGroup = currentFrameGroup { + strongSelf.currentFrameGroup = currentFrameGroup + for target in strongSelf.targets.copyItems() { + target.value?.contents = currentFrameGroup.image.cgImage + } + + let _ = strongSelf.update(advanceFrame: false) + } + } + }) + } + + if advanceFrame { + self.frameIndex += self.frameSkip + } + + if let currentFrameGroup = self.currentFrameGroup, let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) { + for target in self.targets.copyItems() { + if let target = target.value { + target.updateDisplayPlaceholder(displayPlaceholder: false) + target.contentsRect = contentsRect + } + } + } + + return nil + } +} + +public final class MultiAnimationRendererImpl: MultiAnimationRenderer { + private final class GroupContext { + private var frameSkip: Int + private let stateUpdated: () -> Void + + private var itemContexts: [String: ItemAnimationContext] = [:] + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + self.stateUpdated() + } + } + } + + init(frameSkip: Int, stateUpdated: @escaping () -> Void) { + self.frameSkip = frameSkip + self.stateUpdated = stateUpdated + } + + func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { + let itemContext: ItemAnimationContext + if let current = self.itemContexts[itemId] { + itemContext = current + } else { + itemContext = ItemAnimationContext(cache: cache, itemId: itemId, size: size, frameSkip: self.frameSkip, fetch: fetch, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.itemContexts[itemId] = itemContext + } + + let index = itemContext.targets.add(Weak(target)) + itemContext.updateAddedTarget(target: target) + + let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in + Queue.mainQueue().async { + guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else { + return + } + itemContext.targets.remove(index) + if itemContext.targets.isEmpty { + strongSelf.itemContexts.removeValue(forKey: itemId) + } + } + } + + let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in + guard let itemContext = itemContext else { + return + } + itemContext.updateIsPlaying() + } + + return ActionDisposable { [weak self, weak itemContext, weak target] in + guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else { + return + } + if let target = target { + target.deinitCallbacks.remove(deinitIndex) + target.updateStateCallbacks.remove(updateStateIndex) + } + itemContext.targets.remove(index) + if itemContext.targets.isEmpty { + strongSelf.itemContexts.removeValue(forKey: itemId) + } + } + } + + func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + if let item = cache.getSynchronously(sourceId: itemId, size: size) { + guard let frameGroup = FrameGroup(item: item, baseFrameIndex: 0, count: 1, skip: 1) else { + return false + } + + target.contents = frameGroup.image.cgImage + + return true + } else { + return false + } + } + + private func updateIsPlaying() { + var isPlaying = false + for (_, itemContext) in self.itemContexts { + if itemContext.isPlaying { + isPlaying = true + break + } + } + + self.isPlaying = isPlaying + } + + func animationTick() -> [LoadFrameGroupTask] { + var tasks: [LoadFrameGroupTask] = [] + for (_, itemContext) in self.itemContexts { + if itemContext.isPlaying { + if let task = itemContext.animationTick() { + tasks.append(task) + } + } + } + + return tasks + } + } + + private var groupContexts: [String: GroupContext] = [:] + private var frameSkip: Int + private var displayLink: ConstantDisplayLinkAnimator? + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + if self.isPlaying { + if self.displayLink == nil { + self.displayLink = ConstantDisplayLinkAnimator { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.animationTick() + } + self.displayLink?.frameInterval = self.frameSkip + self.displayLink?.isPaused = false + } + } else { + if let displayLink = self.displayLink { + self.displayLink = nil + displayLink.invalidate() + } + } + } + } + } + + public init() { + if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.activeProcessorCount > 2 { + self.frameSkip = 1 + } else { + self.frameSkip = 2 + } + } + + public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContexts[groupId] { + groupContext = current + } else { + groupContext = GroupContext(frameSkip: self.frameSkip, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContexts[groupId] = groupContext + } + + let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch) + + return ActionDisposable { + disposable.dispose() + } + } + + public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + let groupContext: GroupContext + if let current = self.groupContexts[groupId] { + groupContext = current + } else { + groupContext = GroupContext(frameSkip: self.frameSkip, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContexts[groupId] = groupContext + } + + return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size) + } + + private func updateIsPlaying() { + var isPlaying = false + for (_, groupContext) in self.groupContexts { + if groupContext.isPlaying { + isPlaying = true + break + } + } + + self.isPlaying = isPlaying + } + + private func animationTick() { + var tasks: [LoadFrameGroupTask] = [] + for (_, groupContext) in self.groupContexts { + if groupContext.isPlaying { + tasks.append(contentsOf: groupContext.animationTick()) + } + } + + if !tasks.isEmpty { + ItemAnimationContext.queue.async { + var completions: [() -> Void] = [] + for task in tasks { + let complete = task.task() + completions.append(complete) + } + + if !completions.isEmpty { + Queue.mainQueue().async { + for completion in completions { + completion() + } + } + } + } + } + } +} +*/ diff --git a/submodules/TelegramUI/Components/VideoAnimationCache/BUILD b/submodules/TelegramUI/Components/VideoAnimationCache/BUILD new file mode 100644 index 0000000000..7eb3cd35c3 --- /dev/null +++ b/submodules/TelegramUI/Components/VideoAnimationCache/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "VideoAnimationCache", + module_name = "VideoAnimationCache", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/Display:Display", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift new file mode 100644 index 0000000000..ddd56772f7 --- /dev/null +++ b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift @@ -0,0 +1,58 @@ +import Foundation +import UIKit +import AnimationCache +import Display +import AnimatedStickerNode +import SwiftSignalKit + +private func roundUp(_ numToRound: Int, multiple: Int) -> Int { + if multiple == 0 { + return numToRound + } + + let remainder = numToRound % multiple + if remainder == 0 { + return numToRound; + } + + return numToRound + multiple - remainder +} + +public func cacheVideoAnimation(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter) { + writer.queue.async { + guard let frameSource = makeVideoStickerDirectFrameSource(queue: writer.queue, path: path, width: roundUp(width, multiple: 16), height: roundUp(height, multiple: 16), cachePathPrefix: nil) else { + return + } + let frameDuration = 1.0 / Double(frameSource.frameRate) + while true { + if writer.isCancelled { + break + } + if let frame = frameSource.takeFrame(draw: true) { + if case .argb = frame.type { + let bytesPerRow = frame.bytesPerRow + + writer.add(with: { surface in + frame.data.withUnsafeBytes { bytes -> Void in + let sourceArgb = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + if surface.bytesPerRow == bytesPerRow { + memcpy(surface.argb, sourceArgb, min(surface.length, bytes.count)) + } else { + let copyBytesPerRow = min(surface.bytesPerRow, bytesPerRow) + for y in 0 ..< surface.height { + memcpy(surface.argb.advanced(by: y * surface.bytesPerRow), sourceArgb.advanced(by: y * bytesPerRow), copyBytesPerRow) + } + } + } + }, proposedWidth: frame.width, proposedHeight: frame.height, duration: frameDuration) + } else { + break + } + } else { + break + } + } + + writer.finish() + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json new file mode 100644 index 0000000000..f93f8f8fb7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_clear.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/ic_clear.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/ic_clear.pdf new file mode 100644 index 0000000000..559ad92734 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/ic_clear.pdf @@ -0,0 +1,107 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.180420 6.169983 cm +0.000000 0.000000 0.000000 scn +7.131522 16.439991 m +8.015658 17.225891 9.157463 17.660004 10.340398 17.660004 c +19.819601 17.660004 l +22.487137 17.660004 24.649601 15.497540 24.649601 12.830004 c +24.649601 4.830004 l +24.649601 2.162468 22.487137 0.000004 19.819601 0.000004 c +10.340399 0.000004 l +9.157463 0.000004 8.015657 0.434116 7.131521 1.220016 c +0.949850 6.714835 l +-0.316617 7.840584 -0.316617 9.819424 0.949850 10.945172 c +7.131522 16.439991 l +h +10.340398 16.000004 m +9.564020 16.000004 8.814637 15.715089 8.234365 15.199291 c +2.052694 9.704473 l +1.529102 9.239058 1.529102 8.420950 2.052694 7.955535 c +8.234365 2.460716 l +8.814637 1.944919 9.564020 1.660004 10.340399 1.660004 c +19.819601 1.660004 l +21.570343 1.660004 22.989601 3.079261 22.989601 4.830004 c +22.989601 12.830004 l +22.989601 14.580747 21.570343 16.000004 19.819601 16.000004 c +10.340398 16.000004 l +h +10.732702 12.916903 m +11.056838 13.241037 11.582364 13.241037 11.906500 12.916903 c +14.819601 10.003801 l +17.732702 12.916903 l +18.056837 13.241037 18.582365 13.241037 18.906500 12.916903 c +19.230635 12.592768 19.230635 12.067240 18.906500 11.743105 c +15.993399 8.830004 l +18.906500 5.916903 l +19.230635 5.592768 19.230635 5.067240 18.906500 4.743105 c +18.582365 4.418970 18.056837 4.418970 17.732702 4.743105 c +14.819601 7.656206 l +11.906500 4.743105 l +11.582364 4.418970 11.056838 4.418970 10.732702 4.743105 c +10.408567 5.067241 10.408567 5.592767 10.732702 5.916903 c +13.645803 8.830004 l +10.732702 11.743105 l +10.408567 12.067240 10.408567 12.592768 10.732702 12.916903 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1651 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001741 00000 n +0000001764 00000 n +0000001937 00000 n +0000002011 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2070 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputEmojiIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputEmojiIcon.imageset/Contents.json new file mode 100644 index 0000000000..8d1829694d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputEmojiIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_emoji.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputEmojiIcon.imageset/ic_emoji.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputEmojiIcon.imageset/ic_emoji.pdf new file mode 100644 index 0000000000..51dd673605 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputEmojiIcon.imageset/ic_emoji.pdf @@ -0,0 +1,123 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 7.292999 5.741058 cm +0.000000 0.000000 0.000000 scn +7.706251 7.052563 m +9.859218 7.052563 11.806032 7.518116 13.204300 7.852490 c +14.570075 8.179095 15.412500 8.380550 15.412500 7.900764 c +15.412500 3.644719 11.962295 0.194514 7.706251 0.194514 c +3.450206 0.194514 0.000000 3.644719 0.000000 7.900764 c +0.000000 8.380550 0.842425 8.179095 2.208200 7.852490 c +3.606467 7.518116 5.553283 7.052563 7.706251 7.052563 c +h +12.468611 6.348519 m +11.392131 6.066849 9.742197 5.635127 7.697002 5.635127 c +5.651808 5.635127 4.001874 6.066849 2.925394 6.348519 c +2.030582 6.582655 1.532002 6.713113 1.532002 6.405752 c +1.532002 3.455799 6.013958 3.323253 7.697002 3.323253 c +9.586575 3.323253 13.862001 3.456314 13.862001 6.405752 c +13.862001 6.713113 13.363423 6.582655 12.468611 6.348519 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 2.669983 2.669983 cm +0.000000 0.000000 0.000000 scn +12.330000 23.000004 m +6.437122 23.000004 1.660000 18.222881 1.660000 12.330004 c +1.660000 6.437126 6.437122 1.660004 12.330000 1.660004 c +18.222878 1.660004 23.000000 6.437126 23.000000 12.330004 c +23.000000 18.222881 18.222878 23.000004 12.330000 23.000004 c +h +0.000000 12.330004 m +0.000000 19.139675 5.520329 24.660004 12.330000 24.660004 c +19.139671 24.660004 24.660000 19.139675 24.660000 12.330004 c +24.660000 5.520332 19.139671 0.000004 12.330000 0.000004 c +5.520329 0.000004 0.000000 5.520332 0.000000 12.330004 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 10.000000 16.250000 cm +0.000000 0.000000 0.000000 scn +3.000000 1.750000 m +3.000000 0.783502 2.328427 0.000000 1.500000 0.000000 c +0.671573 0.000000 0.000000 0.783502 0.000000 1.750000 c +0.000000 2.716498 0.671573 3.500000 1.500000 3.500000 c +2.328427 3.500000 3.000000 2.716498 3.000000 1.750000 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 17.000000 16.250000 cm +0.000000 0.000000 0.000000 scn +3.000000 1.750000 m +3.000000 0.783502 2.328427 0.000000 1.500000 0.000000 c +0.671573 0.000000 0.000000 0.783502 0.000000 1.750000 c +0.000000 2.716498 0.671573 3.500000 1.500000 3.500000 c +2.328427 3.500000 3.000000 2.716498 3.000000 1.750000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 2167 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002257 00000 n +0000002280 00000 n +0000002453 00000 n +0000002527 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2586 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGifsIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGifsIcon.imageset/Contents.json new file mode 100644 index 0000000000..35222f8570 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGifsIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_gifs.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGifsIcon.imageset/ic_gifs.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGifsIcon.imageset/ic_gifs.pdf new file mode 100644 index 0000000000..410ff918fa --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGifsIcon.imageset/ic_gifs.pdf @@ -0,0 +1,128 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.669983 2.670013 cm +0.000000 0.000000 0.000000 scn +12.330000 22.999989 m +6.437122 22.999989 1.660000 18.222866 1.660000 12.329988 c +1.660000 6.437111 6.437122 1.659988 12.330000 1.659988 c +18.222878 1.659988 23.000000 6.437111 23.000000 12.329988 c +23.000000 18.222866 18.222878 22.999989 12.330000 22.999989 c +h +0.000000 12.329988 m +0.000000 19.139660 5.520329 24.659988 12.330000 24.659988 c +19.139671 24.659988 24.660000 19.139660 24.660000 12.329988 c +24.660000 5.520317 19.139671 -0.000011 12.330000 -0.000011 c +5.520329 -0.000011 0.000000 5.520317 0.000000 12.329988 c +h +17.108725 15.659992 m +17.129999 15.659988 l +18.830000 15.659988 l +19.288397 15.659988 19.660000 15.288384 19.660000 14.829988 c +19.660000 14.371593 19.288397 13.999989 18.830000 13.999989 c +17.160000 13.999989 l +17.160000 13.159988 l +18.830000 13.159988 l +19.288397 13.159988 19.660000 12.788384 19.660000 12.329988 c +19.660000 11.871593 19.288397 11.499989 18.830000 11.499989 c +17.160000 11.499989 l +17.160000 9.829988 l +17.160000 9.371592 16.788397 8.999989 16.330000 8.999989 c +15.871604 8.999989 15.500000 9.371592 15.500000 9.829988 c +15.500000 12.329988 l +15.500000 14.029988 l +15.499996 14.051264 l +15.499962 14.172362 15.499924 14.307926 15.509568 14.425976 c +15.520575 14.560680 15.548159 14.743810 15.644961 14.933796 c +15.772472 15.184052 15.975937 15.387516 16.226192 15.515027 c +16.416180 15.611830 16.599308 15.639414 16.734013 15.650420 c +16.852062 15.660065 16.987627 15.660027 17.108725 15.659992 c +h +13.330000 15.659988 m +13.788396 15.659988 14.160000 15.288384 14.160000 14.829988 c +14.160000 9.829988 l +14.160000 9.371592 13.788396 8.999989 13.330000 8.999989 c +12.871604 8.999989 12.500000 9.371592 12.500000 9.829988 c +12.500000 14.829988 l +12.500000 15.288384 12.871604 15.659988 13.330000 15.659988 c +h +8.330000 15.659988 m +6.490891 15.659988 5.000000 14.169097 5.000000 12.329988 c +5.000000 10.490880 6.490891 8.999989 8.330000 8.999989 c +9.670162 8.999989 11.076591 9.880583 11.385777 11.507551 c +11.393431 11.547824 l +11.397078 11.588655 l +11.427523 11.929440 l +11.486586 12.590580 10.965777 13.159988 10.302004 13.159988 c +9.330000 13.159988 l +8.871603 13.159988 8.500000 12.788385 8.500000 12.329988 c +8.500000 11.871592 8.871603 11.499989 9.330000 11.499989 c +9.657213 11.499989 l +9.421404 10.962639 8.887941 10.659988 8.330000 10.659988 c +7.407684 10.659988 6.660000 11.407673 6.660000 12.329988 c +6.660000 13.252304 7.407684 13.999989 8.330000 13.999989 c +8.609308 13.999989 8.870320 13.932076 9.099771 13.812578 c +9.506336 13.600842 10.007568 13.758780 10.219306 14.165344 c +10.431044 14.571908 10.273106 15.073140 9.866541 15.284878 c +9.405935 15.524760 8.882543 15.659988 8.330000 15.659988 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2783 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002873 00000 n +0000002896 00000 n +0000003069 00000 n +0000003143 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3202 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/Contents.json new file mode 100644 index 0000000000..186f60078b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_search.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/ic_search.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/ic_search.pdf new file mode 100644 index 0000000000..1e6ad4f2b4 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/ic_search.pdf @@ -0,0 +1,82 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.999991 4.221268 cm +0.000000 0.000000 0.000000 scn +2.000000 12.278740 m +2.000000 15.868591 4.910149 18.778740 8.500000 18.778740 c +12.089850 18.778740 15.000000 15.868591 15.000000 12.278740 c +15.000000 8.688890 12.089850 5.778740 8.500000 5.778740 c +4.910149 5.778740 2.000000 8.688890 2.000000 12.278740 c +h +8.500000 20.778740 m +3.805580 20.778740 0.000000 16.973160 0.000000 12.278740 c +0.000000 7.584319 3.805580 3.778740 8.500000 3.778740 c +10.427133 3.778740 12.204475 4.420067 13.630243 5.500938 c +18.676220 0.454960 l +19.131182 0.000000 19.868818 0.000000 20.323780 0.454960 c +20.778740 0.909922 20.778740 1.647558 20.323780 2.102520 c +15.277802 7.148497 l +16.358673 8.574265 17.000000 10.351607 17.000000 12.278740 c +17.000000 16.973160 13.194421 20.778740 8.500000 20.778740 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 864 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000954 00000 n +0000000976 00000 n +0000001149 00000 n +0000001223 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1282 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json new file mode 100644 index 0000000000..6044f8aa11 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_tools.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/ic_tools.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/ic_tools.pdf new file mode 100644 index 0000000000..702905fc0c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/ic_tools.pdf @@ -0,0 +1,177 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.459991 2.485321 cm +0.000000 0.000000 0.000000 scn +11.355469 0.000000 m +13.722656 0.000000 l +14.625000 0.000000 15.316406 0.550781 15.527344 1.417969 c +16.031250 3.609375 l +16.406250 3.738281 l +18.316406 2.566406 l +19.078125 2.085938 19.957031 2.203125 20.601562 2.847656 c +22.242188 4.476562 l +22.886719 5.121094 23.003906 6.011719 22.523438 6.761719 c +21.328125 8.660156 l +21.468750 9.011719 l +23.660156 9.527344 l +24.515625 9.738281 25.078125 10.441406 25.078125 11.332031 c +25.078125 13.652344 l +25.078125 14.542969 24.527344 15.246094 23.660156 15.457031 c +21.492188 15.984375 l +21.339844 16.359375 l +22.535156 18.257812 l +23.015625 19.007812 22.898438 19.886719 22.253906 20.542969 c +20.613281 22.183594 l +19.980469 22.816406 19.101562 22.933594 18.339844 22.464844 c +16.429688 21.292969 l +16.031250 21.445312 l +15.527344 23.636719 l +15.316406 24.503906 14.625000 25.054688 13.722656 25.054688 c +11.355469 25.054688 l +10.453125 25.054688 9.761719 24.503906 9.550781 23.636719 c +9.035156 21.445312 l +8.636719 21.292969 l +6.738281 22.464844 l +5.976562 22.933594 5.085938 22.816406 4.453125 22.183594 c +2.824219 20.542969 l +2.179688 19.886719 2.050781 19.007812 2.542969 18.257812 c +3.726562 16.359375 l +3.585938 15.984375 l +1.417969 15.457031 l +0.550781 15.246094 0.000000 14.542969 0.000000 13.652344 c +0.000000 11.332031 l +0.000000 10.441406 0.562500 9.738281 1.417969 9.527344 c +3.609375 9.011719 l +3.738281 8.660156 l +2.554688 6.761719 l +2.062500 6.011719 2.191406 5.121094 2.835938 4.476562 c +4.464844 2.847656 l +5.109375 2.203125 6.000000 2.085938 6.761719 2.566406 c +8.660156 3.738281 l +9.035156 3.609375 l +9.550781 1.417969 l +9.761719 0.550781 10.453125 0.000000 11.355469 0.000000 c +h +11.542969 1.828125 m +11.343750 1.828125 11.238281 1.910156 11.203125 2.097656 c +10.500000 5.003906 l +9.785156 5.179688 9.117188 5.460938 8.613281 5.777344 c +6.058594 4.207031 l +5.917969 4.101562 5.765625 4.125000 5.625000 4.265625 c +4.242188 5.648438 l +4.113281 5.777344 4.101562 5.917969 4.195312 6.082031 c +5.765625 8.613281 l +5.496094 9.105469 5.191406 9.773438 5.003906 10.488281 c +2.097656 11.179688 l +1.910156 11.214844 1.828125 11.320312 1.828125 11.519531 c +1.828125 13.476562 l +1.828125 13.687500 1.898438 13.781250 2.097656 13.816406 c +4.992188 14.519531 l +5.179688 15.281250 5.531250 15.972656 5.742188 16.406250 c +4.183594 18.937500 l +4.078125 19.113281 4.089844 19.253906 4.218750 19.394531 c +5.613281 20.753906 l +5.753906 20.894531 5.882812 20.917969 6.058594 20.812500 c +8.589844 19.277344 l +9.093750 19.558594 9.808594 19.851562 10.511719 20.050781 c +11.203125 22.957031 l +11.238281 23.144531 11.343750 23.226562 11.542969 23.226562 c +13.535156 23.226562 l +13.734375 23.226562 13.839844 23.144531 13.863281 22.957031 c +14.578125 20.027344 l +15.304688 19.839844 15.937500 19.546875 16.464844 19.265625 c +19.007812 20.812500 l +19.195312 20.917969 19.312500 20.894531 19.464844 20.753906 c +20.847656 19.394531 l +20.988281 19.253906 20.988281 19.113281 20.882812 18.937500 c +19.324219 16.406250 l +19.546875 15.972656 19.886719 15.281250 20.074219 14.519531 c +22.980469 13.816406 l +23.167969 13.781250 23.250000 13.687500 23.250000 13.476562 c +23.250000 11.519531 l +23.250000 11.320312 23.156250 11.214844 22.980469 11.179688 c +20.062500 10.488281 l +19.875000 9.773438 19.582031 9.105469 19.300781 8.613281 c +20.871094 6.082031 l +20.964844 5.917969 20.964844 5.777344 20.824219 5.648438 c +19.453125 4.265625 l +19.300781 4.125000 19.160156 4.101562 19.007812 4.207031 c +16.453125 5.777344 l +15.949219 5.460938 15.292969 5.179688 14.578125 5.003906 c +13.863281 2.097656 l +13.839844 1.910156 13.734375 1.828125 13.535156 1.828125 c +11.542969 1.828125 l +h +12.539062 8.050781 m +14.988281 8.050781 17.003906 10.066406 17.003906 12.527344 c +17.003906 14.964844 14.988281 16.980469 12.539062 16.980469 c +10.089844 16.980469 8.062500 14.964844 8.062500 12.527344 c +8.062500 10.078125 10.078125 8.050781 12.539062 8.050781 c +h +12.539062 9.867188 m +11.085938 9.867188 9.890625 11.062500 9.890625 12.527344 c +9.890625 13.968750 11.085938 15.164062 12.539062 15.164062 c +13.968750 15.164062 15.164062 13.968750 15.164062 12.527344 c +15.164062 11.074219 13.968750 9.867188 12.539062 9.867188 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 4265 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000004355 00000 n +0000004378 00000 n +0000004551 00000 n +0000004625 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +4684 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputStickersIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputStickersIcon.imageset/Contents.json new file mode 100644 index 0000000000..0f561e6972 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputStickersIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_stickers.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputStickersIcon.imageset/ic_stickers.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputStickersIcon.imageset/ic_stickers.pdf new file mode 100644 index 0000000000..24ac31eb23 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputStickersIcon.imageset/ic_stickers.pdf @@ -0,0 +1,431 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.000000 2.336044 cm +0.000000 0.000000 0.000000 scn +12.629680 1.768461 m +12.783916 0.952917 l +12.629680 1.768461 l +h +16.593809 4.257765 m +17.180708 3.670866 l +16.593809 4.257765 l +h +14.895066 2.706814 m +14.427451 3.392551 l +14.895066 2.706814 l +h +21.895494 11.034276 m +22.711039 10.880039 l +21.895494 11.034276 l +h +19.406189 7.070145 m +18.819290 7.657043 l +19.406189 7.070145 l +h +20.957142 8.768889 m +20.271404 9.236505 l +20.957142 8.768889 l +h +0.483443 6.107693 m +-0.289235 5.804592 l +0.483443 6.107693 l +h +4.443737 2.147398 m +4.140636 1.374722 l +4.443737 2.147398 l +h +21.516558 19.220219 m +22.289234 19.523319 l +21.516558 19.220219 l +h +3.962814 22.970737 m +3.602690 23.718542 l +3.962814 22.970737 l +h +0.693218 19.701141 m +-0.054586 20.061266 l +0.693218 19.701141 l +h +11.000000 22.833956 m +11.668203 22.833956 l +11.668203 24.493956 l +11.000000 24.493956 l +11.000000 22.833956 l +h +0.830000 11.995752 m +0.830000 12.663956 l +-0.830000 12.663956 l +-0.830000 11.995752 l +0.830000 11.995752 l +h +18.819290 7.657043 m +16.006910 4.844664 l +17.180708 3.670866 l +19.993088 6.483246 l +18.819290 7.657043 l +h +10.331797 0.833956 m +11.464174 0.833956 12.137112 0.830593 12.783916 0.952917 c +12.475444 2.584003 l +12.017082 2.497318 11.526485 2.493956 10.331797 2.493956 c +10.331797 0.833956 l +h +16.006910 4.844664 m +15.162139 3.999891 14.812858 3.655367 14.427451 3.392551 c +15.362681 2.021076 l +15.906537 2.391941 16.379997 2.870155 17.180708 3.670866 c +16.006910 4.844664 l +h +12.783916 0.952917 m +13.707870 1.127657 14.585788 1.491302 15.362681 2.021076 c +14.427451 3.392551 l +13.839379 2.991537 13.174834 2.716274 12.475444 2.584003 c +12.783916 0.952917 l +h +21.170000 13.332159 m +21.170000 12.137469 21.166637 11.646873 21.079952 11.188512 c +22.711039 10.880039 l +22.833363 11.526844 22.830000 12.199781 22.830000 13.332159 c +21.170000 13.332159 l +h +19.993088 6.483246 m +20.793798 7.283958 21.272015 7.757419 21.642879 8.301274 c +20.271404 9.236505 l +20.008589 8.851098 19.664062 8.501816 18.819290 7.657043 c +19.993088 6.483246 l +h +21.079952 11.188512 m +20.947681 10.489121 20.672419 9.824576 20.271404 9.236505 c +21.642879 8.301274 l +22.172653 9.078168 22.536299 9.956086 22.711039 10.880039 c +21.079952 11.188512 l +h +-0.830000 11.995752 m +-0.830000 10.454670 -0.830408 9.259295 -0.767823 8.296148 c +-0.704785 7.326029 -0.575355 6.533985 -0.289235 5.804592 c +1.256120 6.410791 l +1.058798 6.913816 0.946507 7.513920 0.888684 8.403788 c +0.830408 9.300627 0.830000 10.433072 0.830000 11.995752 c +-0.830000 11.995752 l +h +10.331797 2.493956 m +8.769116 2.493956 7.636671 2.494364 6.739831 2.552639 c +5.849965 2.610462 5.249860 2.722754 4.746836 2.920076 c +4.140636 1.374722 l +4.870029 1.088600 5.662074 0.959171 6.632193 0.896133 c +7.595339 0.833548 8.790714 0.833956 10.331797 0.833956 c +10.331797 2.493956 l +h +-0.289235 5.804592 m +0.506586 3.775846 2.111891 2.170542 4.140636 1.374722 c +4.746836 2.920076 l +3.148195 3.547178 1.883223 4.812151 1.256120 6.410791 c +-0.289235 5.804592 l +h +11.668203 22.833956 m +13.230883 22.833956 14.363329 22.833548 15.260168 22.775272 c +16.150036 22.717449 16.750139 22.605158 17.253164 22.407835 c +17.859364 23.953190 l +17.129971 24.239311 16.337927 24.368740 15.367807 24.431778 c +14.404661 24.494364 13.209285 24.493956 11.668203 24.493956 c +11.668203 22.833956 l +h +22.830000 13.332159 m +22.830000 14.873241 22.830408 16.068617 22.767822 17.031763 c +22.704784 18.001881 22.575356 18.793926 22.289234 19.523319 c +20.743879 18.917120 l +20.941202 18.414095 21.053493 17.813992 21.111317 16.924124 c +21.169592 16.027285 21.170000 14.894838 21.170000 13.332159 c +22.830000 13.332159 l +h +17.253164 22.407835 m +18.851805 21.780733 20.116777 20.515760 20.743879 18.917120 c +22.289234 19.523319 l +21.493414 21.552065 19.888109 23.157370 17.859364 23.953190 c +17.253164 22.407835 l +h +11.000000 24.493956 m +9.147122 24.493956 7.709934 24.494541 6.557355 24.404799 c +5.395766 24.314354 4.453906 24.128466 3.602690 23.718542 c +4.322937 22.222933 l +4.911203 22.506227 5.622035 22.666948 6.686217 22.749807 c +7.759411 22.833370 9.121075 22.833956 11.000000 22.833956 c +11.000000 24.493956 l +h +0.830000 12.663956 m +0.830000 14.542881 0.830586 15.904545 0.914148 16.977737 c +0.997008 18.041920 1.157728 18.752752 1.441022 19.341019 c +-0.054586 20.061266 l +-0.464510 19.210049 -0.650399 18.268190 -0.740843 17.106600 c +-0.830586 15.954021 -0.830000 14.516833 -0.830000 12.663956 c +0.830000 12.663956 l +h +3.602690 23.718542 m +2.004527 22.948908 0.715049 21.659428 -0.054586 20.061266 c +1.441022 19.341019 l +2.047490 20.600363 3.063593 21.616465 4.322937 22.222933 c +3.602690 23.718542 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 14.000000 3.169342 cm +0.000000 0.000000 0.000000 scn +2.895337 1.513170 m +2.155802 1.889982 l +2.895337 1.513170 l +h +2.349067 0.966900 m +2.725879 0.227365 l +2.349067 0.966900 l +h +4.396603 9.526595 m +4.019791 10.266130 l +4.396603 9.526595 l +h +3.304063 8.434055 m +2.564527 8.810867 l +3.304063 8.434055 l +h +11.863758 10.481591 m +11.124223 10.858403 l +11.863758 10.481591 l +h +11.317488 9.935321 m +11.694300 9.195786 l +11.317488 9.935321 l +h +11.170000 12.830658 m +11.170000 11.799080 l +12.830000 11.799080 l +12.830000 12.830658 l +11.170000 12.830658 l +h +10.000000 10.629079 m +7.031581 10.629079 l +7.031581 8.969079 l +10.000000 8.969079 l +10.000000 10.629079 l +h +2.201579 5.799079 m +2.201579 2.830659 l +3.861579 2.830659 l +3.861579 5.799079 l +2.201579 5.799079 l +h +1.031579 1.660658 m +0.000000 1.660658 l +0.000000 0.000658 l +1.031579 0.000658 l +1.031579 1.660658 l +h +2.201579 2.830659 m +2.201579 2.466930 2.200933 2.244051 2.187305 2.077250 c +2.180954 1.999516 2.172969 1.952377 2.166202 1.923643 c +2.162982 1.909968 2.160270 1.901502 2.158613 1.896861 c +2.157021 1.892399 2.156040 1.890450 2.155802 1.889982 c +3.634872 1.136358 l +3.772548 1.406561 3.820568 1.682304 3.841792 1.942073 c +3.862224 2.192151 3.861579 2.494322 3.861579 2.830659 c +2.201579 2.830659 l +h +1.031579 0.000658 m +1.367916 0.000658 1.670086 0.000012 1.920165 0.020445 c +2.179933 0.041669 2.455676 0.089689 2.725879 0.227365 c +1.972255 1.706435 l +1.971787 1.706197 1.969838 1.705216 1.965376 1.703624 c +1.960735 1.701967 1.952269 1.699255 1.938594 1.696035 c +1.909860 1.689268 1.862720 1.681283 1.784988 1.674932 c +1.618186 1.661304 1.395307 1.660658 1.031579 1.660658 c +1.031579 0.000658 l +h +2.155802 1.889982 m +2.115535 1.810954 2.051283 1.746702 1.972255 1.706435 c +2.725879 0.227365 l +3.117256 0.426782 3.435456 0.744981 3.634872 1.136358 c +2.155802 1.889982 l +h +7.031581 10.629079 m +6.345211 10.629079 5.780515 10.629725 5.321996 10.592262 c +4.853787 10.554008 4.423688 10.471928 4.019791 10.266130 c +4.773415 8.787060 l +4.904296 8.853747 5.091620 8.907908 5.457173 8.937775 c +5.832415 8.968433 6.317819 8.969079 7.031581 8.969079 c +7.031581 10.629079 l +h +3.861579 5.799079 m +3.861579 6.512841 3.862224 6.998244 3.892883 7.373486 c +3.922750 7.739038 3.976911 7.926362 4.043598 8.057243 c +2.564527 8.810867 l +2.358731 8.406969 2.276650 7.976871 2.238396 7.508663 c +2.200933 7.050144 2.201579 6.485449 2.201579 5.799079 c +3.861579 5.799079 l +h +4.019791 10.266130 m +3.393211 9.946873 2.883785 9.437447 2.564527 8.810867 c +4.043598 8.057243 l +4.203707 8.371473 4.459184 8.626951 4.773415 8.787060 c +4.019791 10.266130 l +h +11.170000 11.799080 m +11.170000 11.435350 11.169354 11.212472 11.155726 11.045670 c +11.149375 10.967937 11.141390 10.920798 11.134623 10.892064 c +11.131403 10.878389 11.128691 10.869923 11.127034 10.865282 c +11.125442 10.860820 11.124461 10.858871 11.124223 10.858403 c +12.603293 10.104778 l +12.740969 10.374982 12.788989 10.650725 12.810213 10.910493 c +12.830646 11.160572 12.830000 11.462742 12.830000 11.799080 c +11.170000 11.799080 l +h +10.000000 8.969079 m +10.336337 8.969079 10.638507 8.968433 10.888586 8.988866 c +11.148354 9.010090 11.424097 9.058110 11.694300 9.195786 c +10.940676 10.674856 l +10.940208 10.674618 10.938259 10.673637 10.933797 10.672045 c +10.929156 10.670388 10.920690 10.667677 10.907015 10.664455 c +10.878281 10.657689 10.831141 10.649704 10.753409 10.643353 c +10.586607 10.629725 10.363729 10.629079 10.000000 10.629079 c +10.000000 8.969079 l +h +11.124223 10.858403 m +11.083956 10.779375 11.019704 10.715123 10.940676 10.674856 c +11.694300 9.195786 l +12.085677 9.395203 12.403876 9.713402 12.603293 10.104778 c +11.124223 10.858403 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 10.000000 16.250000 cm +0.000000 0.000000 0.000000 scn +3.000000 1.750000 m +3.000000 0.783502 2.328427 0.000000 1.500000 0.000000 c +0.671573 0.000000 0.000000 0.783502 0.000000 1.750000 c +0.000000 2.716498 0.671573 3.500000 1.500000 3.500000 c +2.328427 3.500000 3.000000 2.716498 3.000000 1.750000 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 17.000000 16.250000 cm +0.000000 0.000000 0.000000 scn +3.000000 1.750000 m +3.000000 0.783502 2.328427 0.000000 1.500000 0.000000 c +0.671573 0.000000 0.000000 0.783502 0.000000 1.750000 c +0.000000 2.716498 0.671573 3.500000 1.500000 3.500000 c +2.328427 3.500000 3.000000 2.716498 3.000000 1.750000 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 11.000000 8.230942 cm +0.000000 0.000000 0.000000 scn +0.664000 4.267058 m +0.388962 4.633775 -0.131283 4.708096 -0.498000 4.433058 c +-0.864717 4.158020 -0.939038 3.637775 -0.664000 3.271058 c +0.664000 4.267058 l +h +4.000000 1.769054 m +3.999999 0.939054 l +4.000000 0.939054 l +4.000000 1.769054 l +h +6.332036 1.389412 m +6.752154 1.572791 6.944070 2.062022 6.760692 2.482140 c +6.577314 2.902259 6.088083 3.094175 5.667964 2.910796 c +6.332036 1.389412 l +h +0.000000 3.769058 m +-0.664000 3.271058 -0.663799 3.270791 -0.663593 3.270516 c +-0.663516 3.270414 -0.663304 3.270131 -0.663150 3.269927 c +-0.662843 3.269519 -0.662512 3.269080 -0.662157 3.268610 c +-0.661448 3.267670 -0.660643 3.266607 -0.659743 3.265423 c +-0.657943 3.263054 -0.655763 3.260200 -0.653203 3.256877 c +-0.648084 3.250230 -0.641445 3.241700 -0.633302 3.231414 c +-0.617020 3.210848 -0.594689 3.183217 -0.566423 3.149538 c +-0.509945 3.082245 -0.429440 2.990409 -0.325813 2.882277 c +-0.119243 2.666726 0.183137 2.382302 0.574317 2.097807 c +1.353481 1.531142 2.521308 0.939057 3.999999 0.939054 c +4.000001 2.599054 l +2.978692 2.599056 2.146519 3.006973 1.550683 3.440309 c +1.254363 3.655814 1.025493 3.871391 0.872688 4.030839 c +0.796627 4.110207 0.740414 4.174622 0.705095 4.216704 c +0.687462 4.237712 0.675126 4.253050 0.668214 4.261781 c +0.664761 4.266143 0.662671 4.268843 0.661961 4.269765 c +0.661607 4.270226 0.661598 4.270240 0.661937 4.269794 c +0.662106 4.269571 0.662363 4.269233 0.662706 4.268778 c +0.662878 4.268550 0.663072 4.268293 0.663288 4.268007 c +0.663395 4.267863 0.663574 4.267626 0.663627 4.267555 c +0.663811 4.267310 0.664000 4.267058 0.000000 3.769058 c +h +4.000000 0.939054 m +4.844985 0.939054 5.627935 1.082078 6.332036 1.389412 c +5.667964 2.910796 l +5.208086 2.710063 4.657703 2.599054 4.000000 2.599054 c +4.000000 0.939054 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 10900 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000010990 00000 n +0000011014 00000 n +0000011187 00000 n +0000011261 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +11320 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 5fd0ef4b1a..5ee132edaf 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -816,7 +816,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, enqueueMessage: { message in self?.sendMessages([message]) }, sendSticker: canSendMessagesToChat(strongSelf.presentationInterfaceState) ? { fileReference, sourceNode, sourceRect in - return self?.controllerInteraction?.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) ?? false + return self?.controllerInteraction?.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) ?? false } : nil, setupTemporaryHiddenMedia: { signal, centralIndex, galleryMedia in if let strongSelf = self { strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal |> deliverOnMainQueue).start(next: { entry in @@ -917,8 +917,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G transitionCompletion() }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -1494,13 +1494,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G attributes.append(TextEntitiesMessageAttribute(entities: entities)) } strongSelf.sendMessages([.message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil)]) - }, sendSticker: { [weak self] fileReference, silentPosting, schedule, query, clearInput, sourceNode, sourceRect in + }, sendSticker: { [weak self] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in guard let strongSelf = self else { return false } if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceView, sourceRect) return false } @@ -1517,7 +1517,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } var shouldAnimateMessageTransition = strongSelf.chatDisplayNode.shouldAnimateMessageTransition - if sourceNode is ChatEmptyNodeStickerContentNode { + if let _ = sourceView.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode { shouldAnimateMessageTransition = true } @@ -1545,7 +1545,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, shouldAnimateMessageTransition ? correlationId : nil) if shouldAnimateMessageTransition { - if let sourceNode = sourceNode as? ChatMediaInputStickerGridItemNode { + if let sourceNode = sourceView.asyncdisplaykit_node as? ChatMediaInputStickerGridItemNode { strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: { guard let strongSelf = self else { return @@ -1562,12 +1562,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return current }) }) - } else if let sourceNode = sourceNode as? HorizontalStickerGridItemNode { + } else if let sourceNode = sourceView.asyncdisplaykit_node as? HorizontalStickerGridItemNode { strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .mediaPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) - } else if let sourceNode = sourceNode as? StickerPaneSearchStickerItemNode { + } else if let sourceNode = sourceView.asyncdisplaykit_node as? StickerPaneSearchStickerItemNode { strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanelSearch(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) - } else if let sourceNode = sourceNode as? ChatEmptyNodeStickerContentNode { + } else if let sourceNode = sourceView.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode { strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .emptyPanel(itemNode: sourceNode), replyPanel: nil), initiated: {}) + } else if let sourceLayer = sourceLayer { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .universal(sourceContainerView: sourceView, sourceRect: sourceRect, sourceLayer: sourceLayer), replyPanel: replyPanel), initiated: { + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) + }) } } @@ -1587,10 +1604,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.sendMessages(transformedMessages) } return true - }, sendGif: { [weak self] fileReference, sourceNode, sourceRect, silentPosting, schedule in + }, sendGif: { [weak self] fileReference, sourceView, sourceRect, silentPosting, schedule in if let strongSelf = self { if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceView, sourceRect) return false } @@ -1632,7 +1649,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode.view, sourceRect) return false } @@ -2947,7 +2964,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { if let _ = strongSelf.presentationInterfaceState.slowmodeState { if let rect = strongSelf.chatDisplayNode.frameForInputActionButton() { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode, rect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode.view, rect) } return } else { @@ -3288,8 +3305,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] { legacyMediaEditor(context: strongSelf.context, peer: peer, media: mediaReference, initialCaption: NSAttributedString(string: message.text), snapshots: [], transitionCompletion: nil, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -6128,7 +6145,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if strongSelf.presentationInterfaceState.interfaceState.editMessage == nil, let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { if let rect = strongSelf.chatDisplayNode.frameForAttachmentButton() { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode, rect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode.view, rect) } return } @@ -7197,7 +7214,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(node, rect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(node.view, rect) return false } @@ -7581,9 +7598,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - }, sendSticker: { [weak self] file, clearInput, sourceNode, sourceRect in + }, sendSticker: { [weak self] file, clearInput, sourceView, sourceRect, sourceLayer in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { - return strongSelf.controllerInteraction?.sendSticker(file, false, false, nil, clearInput, sourceNode, sourceRect) ?? false + return strongSelf.controllerInteraction?.sendSticker(file, false, false, nil, clearInput, sourceView, sourceRect, sourceLayer) ?? false } else { return false } @@ -8240,11 +8257,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) })] strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.ReportGroupLocation_Title, text: strongSelf.presentationData.strings.ReportGroupLocation_Text, actions: actions), in: .window(.root)) - }, displaySlowmodeTooltip: { [weak self] node, nodeRect in + }, displaySlowmodeTooltip: { [weak self] sourceView, nodeRect in guard let strongSelf = self, let slowmodeState = strongSelf.presentationInterfaceState.slowmodeState else { return } - let rect = node.view.convert(nodeRect, to: strongSelf.view) + let rect = sourceView.convert(nodeRect, to: strongSelf.view) if let slowmodeTooltipController = strongSelf.slowmodeTooltipController { if let arguments = slowmodeTooltipController.presentationArguments as? TooltipControllerPresentationArguments, case let .node(f) = arguments.sourceAndRect, let (previousNode, previousRect) = f() { if previousNode === strongSelf.chatDisplayNode && previousRect == rect { @@ -8559,6 +8576,35 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return $0.updatedShowWebView(f($0.showWebView)) }) } + }, insertText: { [weak self] text in + guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction else { + return + } + if !strongSelf.chatDisplayNode.isTextInputPanelActive { + return + } + + interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in + let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) + + let range = textInputState.selectionRange + inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: text) + + let selectionPosition = range.lowerBound + (text.string as NSString).length + + return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode) + } + }, backwardsDeleteText: { [weak self] in + guard let strongSelf = self else { + return + } + if !strongSelf.chatDisplayNode.isTextInputPanelActive { + return + } + guard let textInputPanelNode = strongSelf.chatDisplayNode.textInputPanelNode else { + return + } + textInputPanelNode.backwardsDeleteText() }, chatController: { [weak self] in return self }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get())) @@ -10759,8 +10805,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -11364,8 +11410,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -11462,8 +11508,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -11672,8 +11718,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } controller.presentStickers = { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -11769,8 +11815,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) controller.presentStickers = { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -11816,8 +11862,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -11873,8 +11919,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) controller.presentStickers = { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -12723,8 +12769,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -12944,8 +12990,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.sendMessages([updatedMessage]) } - }, displaySlowmodeTooltip: { [weak self] node, rect in - self?.interfaceInteraction?.displaySlowmodeTooltip(node, rect) + }, displaySlowmodeTooltip: { [weak self] view, rect in + self?.interfaceInteraction?.displaySlowmodeTooltip(view, rect) }, presentSchedulePicker: { [weak self] done in if let strongSelf = self { strongSelf.presentScheduleTimePicker(completion: { [weak self] time in @@ -13131,7 +13177,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let _ = self.presentationInterfaceState.slowmodeState, !isScheduledMessages { if let rect = self.chatDisplayNode.frameForInputActionButton() { - self.interfaceInteraction?.displaySlowmodeTooltip(self.chatDisplayNode, rect) + self.interfaceInteraction?.displaySlowmodeTooltip(self.chatDisplayNode.view, rect) } return } @@ -14821,8 +14867,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G default: break } - }, sendFile: nil, sendSticker: { [weak self] f, sourceNode, sourceRect in - return self?.interfaceInteraction?.sendSticker(f, true, sourceNode, sourceRect) ?? false + }, sendFile: nil, sendSticker: { [weak self] f, sourceView, sourceRect in + return self?.interfaceInteraction?.sendSticker(f, true, sourceView, sourceRect, nil) ?? false }, requestMessageActionUrlAuth: { [weak self] subject in if case let .url(url) = subject { self?.controllerInteraction?.requestMessageActionUrlAuth(url, subject) diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index d950b8235e..6beeb74fd9 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -73,8 +73,8 @@ public final class ChatControllerInteraction { let toggleMessagesSelection: ([MessageId], Bool) -> Void let sendCurrentMessage: (Bool) -> Void let sendMessage: (String) -> Void - let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, ASDisplayNode, CGRect) -> Bool - let sendGif: (FileMediaReference, ASDisplayNode, CGRect, Bool, Bool) -> Bool + let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Bool + let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect, Bool) -> Bool let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void @@ -176,8 +176,8 @@ public final class ChatControllerInteraction { toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, - sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, ASDisplayNode, CGRect) -> Bool, - sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect, Bool, Bool) -> Bool, + sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Bool, + sendGif: @escaping (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool, sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect, Bool) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageActionUrlSubject) -> Void, diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 4b963f0286..dad5427e80 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -83,7 +83,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let backgroundNode: WallpaperBackgroundNode let historyNode: ChatHistoryListNode - //let historyScrollingArea: SparseDiscreteScrollingArea var blurredHistoryNode: ASImageNode? let historyNodeContainer: ASDisplayNode let loadingNode: ChatLoadingNode @@ -100,7 +99,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let inputPanelContainerNode: SparseNode private let inputPanelBackgroundNode: NavigationBackgroundNode + private var intrinsicInputPanelBackgroundNodeSize: CGSize? private let inputPanelBackgroundSeparatorNode: ASDisplayNode + private let inputPanelBottomBackgroundSeparatorNode: ASDisplayNode private var plainInputSeparatorAlpha: CGFloat? private var usePlainInputSeparator: Bool @@ -122,7 +123,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var disappearingNode: ChatInputNode? private(set) var textInputPanelNode: ChatTextInputPanelNode? + private var inputMediaNode: ChatMediaInputNode? + private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? + private var inputMediaNodeDataPromise = Promise() + private var didInitializeInputMediaNodeDataPromise: Bool = false + private var inputMediaNodeDataDisposable: Disposable? let navigateButtons: ChatHistoryNavigationButtons @@ -378,6 +384,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelSeparatorColor self.inputPanelBackgroundSeparatorNode.isLayerBacked = true + self.inputPanelBottomBackgroundSeparatorNode = ASDisplayNode() + self.inputPanelBottomBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputMediaPanel.panelSeparatorColor + self.inputPanelBottomBackgroundSeparatorNode.isLayerBacked = true + self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat) self.navigateButtons.accessibilityElementsHidden = true @@ -499,6 +509,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.addSubnode(self.inputPanelContainerNode) self.inputPanelContainerNode.addSubnode(self.inputPanelBackgroundNode) self.inputPanelContainerNode.addSubnode(self.inputPanelBackgroundSeparatorNode) + self.inputPanelContainerNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode) self.addSubnode(self.inputContextPanelContainer) @@ -553,12 +564,21 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.textInputPanelNode?.updateActivity = { [weak self] in self?.updateTypingActivity(true) } + + self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.inputMediaNodeData = value + }) } deinit { self.interactiveEmojisDisposable?.dispose() self.openStickersDisposable?.dispose() self.displayVideoUnmuteTipDisposable?.dispose() + self.inputMediaNodeDataDisposable?.dispose() } override func didLoad() { @@ -886,12 +906,17 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let maximumInputNodeHeight = layout.size.height - max(navigationBarHeight + (titleAccessoryPanelBackgroundHeight ?? 0.0), layout.safeInsets.top) - inputPanelNodeBaseHeight var dismissedInputNode: ChatInputNode? + var dismissedInputNodeExternalTopPanelContainer: UIView? var immediatelyLayoutInputNodeAndAnimateAppearance = false var inputNodeHeightAndOverflow: (CGFloat, CGFloat)? - if let inputNode = inputNodeForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentNode: self.inputNode, interfaceInteraction: self.interfaceInteraction, inputMediaNode: self.inputMediaNode, controllerInteraction: self.controllerInteraction, inputPanelNode: self.inputPanelNode) { - if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { - if inputPanelNode.isFocused { - self.context.sharedContext.mainWindow?.simulateKeyboardDismiss(transition: .animated(duration: 0.5, curve: .spring)) + if let inputNode = inputNodeForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentNode: self.inputNode, interfaceInteraction: self.interfaceInteraction, inputMediaNode: self.inputMediaNode, controllerInteraction: self.controllerInteraction, inputPanelNode: self.inputPanelNode, makeMediaInputNode: { + return self.makeMediaInputNode() + }) { + if self.inputMediaNode != nil { + if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { + if inputPanelNode.isFocused { + self.context.sharedContext.mainWindow?.simulateKeyboardDismiss(transition: .animated(duration: 0.5, curve: .spring)) + } } } if let inputMediaNode = inputNode as? ChatMediaInputNode, self.inputMediaNode == nil { @@ -901,20 +926,39 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } if self.inputNode != inputNode { + inputNode.topBackgroundExtensionUpdated = { [weak self] transition in + self?.updateInputPanelBackgroundExtension(transition: transition) + } + dismissedInputNode = self.inputNode + dismissedInputNodeExternalTopPanelContainer = self.inputNode?.externalTopPanelContainer self.inputNode = inputNode inputNode.alpha = 1.0 inputNode.layer.removeAnimation(forKey: "opacity") immediatelyLayoutInputNodeAndAnimateAppearance = true - if let inputPanelNode = self.inputPanelNode, inputPanelNode.supernode != nil { - self.inputPanelContainerNode.insertSubnode(inputNode, aboveSubnode: inputPanelNode) + + if self.inputMediaNode != nil { + if let inputPanelNode = self.inputPanelNode, inputPanelNode.supernode != nil { + self.inputPanelContainerNode.insertSubnode(inputNode, belowSubnode: inputPanelNode) + } else { + self.inputPanelContainerNode.insertSubnode(inputNode, belowSubnode: self.inputPanelBackgroundNode) + } } else { - self.inputPanelContainerNode.insertSubnode(inputNode, aboveSubnode: self.inputPanelBackgroundNode) + self.inputPanelContainerNode.insertSubnode(inputNode, belowSubnode: self.inputPanelBackgroundNode) + } + + if let externalTopPanelContainer = inputNode.externalTopPanelContainer { + if let inputPanelNode = self.inputPanelNode, inputPanelNode.supernode != nil { + self.inputPanelContainerNode.view.insertSubview(externalTopPanelContainer, belowSubview: inputPanelNode.view) + } else { + self.inputPanelContainerNode.view.addSubview(externalTopPanelContainer) + } } } inputNodeHeightAndOverflow = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelNodeBaseHeight, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: layout.deviceMetrics, isVisible: self.isInFocus) } else if let inputNode = self.inputNode { dismissedInputNode = inputNode + dismissedInputNodeExternalTopPanelContainer = inputNode.externalTopPanelContainer self.inputNode = nil } @@ -1357,7 +1401,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let previousInputPanelBackgroundFrame = self.inputPanelBackgroundNode.frame transition.updateFrame(node: self.inputPanelContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame) - self.inputPanelBackgroundNode.update(size: CGSize(width: apparentInputBackgroundFrame.size.width, height: apparentInputBackgroundFrame.size.height + 41.0 + 31.0), transition: transition) + + let intrinsicInputPanelBackgroundNodeSize = CGSize(width: apparentInputBackgroundFrame.size.width, height: apparentInputBackgroundFrame.size.height) + self.intrinsicInputPanelBackgroundNodeSize = intrinsicInputPanelBackgroundNodeSize + var inputPanelBackgroundExtension: CGFloat = 0.0 + if let inputNode = self.inputNode { + inputPanelBackgroundExtension = inputNode.topBackgroundExtension + } + self.inputPanelBackgroundNode.update(size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: intrinsicInputPanelBackgroundNodeSize.height + inputPanelBackgroundExtension), transition: transition) + transition.updateFrame(node: self.inputPanelBottomBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.inputPanelBackgroundNode.frame.maxY + inputPanelBackgroundExtension), size: CGSize(width: self.inputPanelBackgroundNode.bounds.width, height: UIScreenPixel))) + transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: CGSize(width: apparentInputBackgroundFrame.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.navigateButtons, frame: apparentNavigateButtonsFrame) @@ -1437,8 +1490,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { adjustedForPreviousInputHeightFrame.origin.y += heightDifference inputNode.frame = adjustedForPreviousInputHeightFrame transition.updateFrame(node: inputNode, frame: inputNodeFrame) + + if let externalTopPanelContainer = inputNode.externalTopPanelContainer { + externalTopPanelContainer.frame = CGRect(origin: adjustedForPreviousInputHeightFrame.origin, size: CGSize(width: adjustedForPreviousInputHeightFrame.width, height: 0.0)) + transition.updateFrame(view: externalTopPanelContainer, frame: CGRect(origin: inputNodeFrame.origin, size: CGSize(width: inputNodeFrame.width, height: 0.0))) + } } else { transition.updateFrame(node: inputNode, frame: inputNodeFrame) + if let externalTopPanelContainer = inputNode.externalTopPanelContainer { + transition.updateFrame(view: externalTopPanelContainer, frame: CGRect(origin: inputNodeFrame.origin, size: CGSize(width: inputNodeFrame.width, height: 0.0))) + } } } @@ -1628,6 +1689,24 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { targetY = layout.size.height } + + if let dismissedInputNodeExternalTopPanelContainer = dismissedInputNodeExternalTopPanelContainer { + transition.updateFrame(view: dismissedInputNodeExternalTopPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: targetY), size: CGSize(width: layout.size.width, height: 0.0)), force: true, completion: { [weak self, weak dismissedInputNodeExternalTopPanelContainer] completed in + if let strongSelf = self, let dismissedInputNodeExternalTopPanelContainer = dismissedInputNodeExternalTopPanelContainer { + if strongSelf.inputNode?.externalTopPanelContainer !== dismissedInputNodeExternalTopPanelContainer { + dismissedInputNodeExternalTopPanelContainer.alpha = 0.0 + dismissedInputNodeExternalTopPanelContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak dismissedInputNodeExternalTopPanelContainer] completed in + if completed, let strongSelf = self, let dismissedInputNodeExternalTopPanelContainer = dismissedInputNodeExternalTopPanelContainer { + if strongSelf.inputNode?.externalTopPanelContainer !== dismissedInputNodeExternalTopPanelContainer { + dismissedInputNodeExternalTopPanelContainer.removeFromSuperview() + } + } + }) + } + } + }) + } + transition.updateFrame(node: dismissedInputNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetY), size: CGSize(width: layout.size.width, height: max(insets.bottom, dismissedInputNode.bounds.size.height))), force: true, completion: { [weak self, weak dismissedInputNode] completed in if let dismissedInputNode = dismissedInputNode { if let strongSelf = self { @@ -1688,6 +1767,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { //self.notifyTransitionCompletionListeners(transition: transition) } + private func updateInputPanelBackgroundExtension(transition: ContainedViewLayoutTransition) { + guard let intrinsicInputPanelBackgroundNodeSize = self.intrinsicInputPanelBackgroundNodeSize else { + return + } + + var extensionValue: CGFloat = 0.0 + if let inputNode = self.inputNode { + extensionValue = inputNode.topBackgroundExtension + } + + self.inputPanelBackgroundNode.update(size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: intrinsicInputPanelBackgroundNodeSize.height + extensionValue), transition: transition) + transition.updateFrame(node: self.inputPanelBottomBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.inputPanelBackgroundNode.frame.maxY + extensionValue), size: CGSize(width: self.inputPanelBackgroundNode.bounds.width, height: UIScreenPixel))) + } + private func notifyTransitionCompletionListeners(transition: ContainedViewLayoutTransition) { if !self.onLayoutCompletions.isEmpty { let onLayoutCompletions = self.onLayoutCompletions @@ -1700,14 +1793,28 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private func chatPresentationInterfaceStateRequiresInputFocus(_ state: ChatPresentationInterfaceState) -> Bool { switch state.inputMode { - case .text: - if state.interfaceState.selectionState != nil { - return false - } else { - return true - } - default: + case .text: + if state.interfaceState.selectionState != nil { return false + } else { + return true + } + case .media: + return true + default: + return false + } + } + + private let emptyInputView = UIView() + private func chatPresentationInterfaceStateInputView(_ state: ChatPresentationInterfaceState) -> UIView? { + switch state.inputMode { + case .text: + return nil + case .media: + return self.emptyInputView + default: + return nil } } @@ -1730,6 +1837,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) let updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) + let updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState self.chatPresentationInterfaceState = chatPresentationInterfaceState @@ -1746,6 +1854,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } self.updatePlainInputSeparator(transition: .immediate) self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelSeparatorColor + self.inputPanelBottomBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputMediaPanel.panelSeparatorColor self.backgroundNode.updateBubbleTheme(bubbleTheme: chatPresentationInterfaceState.theme, bubbleCorners: chatPresentationInterfaceState.bubbleCorners) } @@ -1840,6 +1949,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.navigationBar?.setContentNode(nil, animated: transitionIsAnimated) } + if let textView = self.textInputPanelNode?.textInputNode?.textView { + let updatedInputView = self.chatPresentationInterfaceStateInputView(chatPresentationInterfaceState) + if textView.inputView !== updatedInputView { + textView.inputView = updatedInputView + if textView.isFirstResponder { + textView.reloadInputViews() + } + } + } + if updatedInputFocus { if !self.ignoreUpdateHeight { self.scheduleLayoutTransitionRequest(layoutTransition) @@ -1963,8 +2082,30 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.setNeedsLayout() } + private func makeMediaInputNode() -> ChatInputNode? { + guard let inputMediaNodeData = self.inputMediaNodeData else { + return nil + } + + var peerId: PeerId? + if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation { + peerId = id + } + let _ = peerId + + let inputNode = ChatEntityKeyboardInputNode(context: self.context, currentInputData: inputMediaNodeData, updatedInputData: self.inputMediaNodeDataPromise.get()) + + return inputNode + } + func loadInputPanels(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { - if self.inputMediaNode == nil { + if !self.didInitializeInputMediaNodeDataPromise, let interfaceInteraction = self.interfaceInteraction { + self.didInitializeInputMediaNodeDataPromise = true + + self.inputMediaNodeDataPromise.set(ChatEntityKeyboardInputNode.inputData(context: self.context, interfaceInteraction: interfaceInteraction, controllerInteraction: self.controllerInteraction)) + } + + if self.inputMediaNode == nil && !self.context.sharedContext.immediateExperimentalUISettings.inlineStickers { let peerId: PeerId? = self.chatPresentationInterfaceState.chatLocation.peerId let inputNode = ChatMediaInputNode(context: self.context, peerId: peerId, chatLocation: self.chatPresentationInterfaceState.chatLocation, controllerInteraction: self.controllerInteraction, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, theme: theme, strings: strings, fontSize: fontSize, gifPaneIsActiveUpdated: { [weak self] value in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { @@ -1989,9 +2130,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let (validLayout, _) = self.validLayout { let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, inputHeight: validLayout.inputHeight ?? 0.0, maximumHeight: validLayout.standardInputHeight, inputPanelHeight: 44.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: validLayout.deviceMetrics, isVisible: false) } - - self.textInputPanelNode?.loadTextInputNodeIfNeeded() } + + self.textInputPanelNode?.loadTextInputNodeIfNeeded() } func currentInputPanelFrame() -> CGRect? { @@ -2376,15 +2517,31 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } func openStickers() { - if let inputMediaNode = self.inputMediaNode, self.openStickersDisposable == nil { - self.openStickersDisposable = (inputMediaNode.ready - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] in - self?.openStickersDisposable = nil - self?.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in - return (.media(mode: .other, expanded: nil, focused: false), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + if let inputMediaNode = self.inputMediaNode { + if self.openStickersDisposable == nil { + self.openStickersDisposable = (inputMediaNode.ready + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] in + self?.openStickersDisposable = nil + self?.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.media(mode: .other, expanded: nil, focused: false), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + }) }) - }) + } + } else { + if self.openStickersDisposable == nil { + self.openStickersDisposable = (self.inputMediaNodeDataPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + + strongSelf.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.media(mode: .other, expanded: nil, focused: false), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + }) + }) + } } } @@ -2409,7 +2566,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let _ = effectivePresentationInterfaceState.slowmodeState, !isScheduledMessages && scheduleTime == nil { if let rect = self.frameForInputActionButton() { - self.interfaceInteraction?.displaySlowmodeTooltip(self, rect) + self.interfaceInteraction?.displaySlowmodeTooltip(self.view, rect) } return } diff --git a/submodules/TelegramUI/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Sources/ChatEmptyNode.swift index 5b87ffd1fe..bf7c3fe2f8 100644 --- a/submodules/TelegramUI/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Sources/ChatEmptyNode.swift @@ -134,7 +134,7 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke guard let stickerItem = self.stickerItem else { return } - let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self, self.stickerNode.bounds) + let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds, nil) } func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { @@ -303,7 +303,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC guard let stickerItem = self.stickerItem else { return } - let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self, self.stickerNode.bounds) + let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds, nil) } func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift new file mode 100644 index 0000000000..0fbdb124a7 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -0,0 +1,441 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import AccountContext +import ChatPresentationInterfaceState +import ComponentFlow +import EntityKeyboard +import AnimationCache +import MultiAnimationRenderer +import Postbox +import TelegramCore +import ComponentDisplayAdapters +import SettingsUI +import TextFormat + +final class ChatEntityKeyboardInputNode: ChatInputNode { + struct InputData: Equatable { + let emoji: EmojiPagerContentComponent + let stickers: EmojiPagerContentComponent + let gifs: GifPagerContentComponent + + init( + emoji: EmojiPagerContentComponent, + stickers: EmojiPagerContentComponent, + gifs: GifPagerContentComponent + ) { + self.emoji = emoji + self.stickers = stickers + self.gifs = gifs + } + } + + static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction) -> Signal { + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let isPremiumDisabled = premiumConfiguration.isPremiumDisabled + + let emojiInputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { [weak interfaceInteraction] item, _, _, _ in + guard let interfaceInteraction = interfaceInteraction else { + return + } + var text = "." + var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? + loop: for attribute in item.file.attributes { + switch attribute { + case let .Sticker(displayText, packReference, _): + text = displayText + if let packReference = packReference { + emojiAttribute = ChatTextInputTextCustomEmojiAttribute(stickerPack: packReference, fileId: item.file.fileId.id) + break loop + } + default: + break + } + } + + if let emojiAttribute = emojiAttribute { + interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) + } + }, + deleteBackwards: { [weak interfaceInteraction] in + guard let interfaceInteraction = interfaceInteraction else { + return + } + interfaceInteraction.backwardsDeleteText() + }, + openStickerSettings: { + } + ) + let stickerInputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { [weak interfaceInteraction] item, view, rect, layer in + guard let interfaceInteraction = interfaceInteraction else { + return + } + let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, view, rect, layer) + }, + deleteBackwards: { [weak interfaceInteraction] in + guard let interfaceInteraction = interfaceInteraction else { + return + } + interfaceInteraction.backwardsDeleteText() + }, + openStickerSettings: { [weak controllerInteraction] in + guard let controllerInteraction = controllerInteraction else { + return + } + let controller = installedStickerPacksController(context: context, mode: .modal) + controller.navigationPresentation = .modal + controllerInteraction.navigationController()?.pushViewController(controller) + } + ) + let gifInputInteraction = GifPagerContentComponent.InputInteraction( + performItemAction: { [weak controllerInteraction] item, view, rect in + guard let controllerInteraction = controllerInteraction else { + return + } + let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false) + } + ) + + let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + let animationRenderer = MultiAnimationRendererImpl() + + let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers] + let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] + + let emojiItems: Signal = context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000) + |> map { view -> EmojiPagerContentComponent in + struct ItemGroup { + var id: AnyHashable + var items: [EmojiPagerContentComponent.Item] + } + var itemGroups: [ItemGroup] = [] + var itemGroupIndexById: [AnyHashable: Int] = [:] + + var emojiCollectionIds = Set() + for (id, info, _) in view.collectionInfos { + if let info = info as? StickerPackCollectionInfo { + if info.shortName.lowercased().contains("emoji") { + emojiCollectionIds.insert(id) + } + } + } + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + if item.file.isAnimatedSticker || item.file.isVideoSticker { + if emojiCollectionIds.contains(entry.index.collectionId) { + let resultItem = EmojiPagerContentComponent.Item( + emoji: "", + file: item.file + ) + + let groupId = entry.index.collectionId + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) + } + } + } + } + + return EmojiPagerContentComponent( + context: context, + animationCache: animationCache, + animationRenderer: animationRenderer, + inputInteraction: emojiInputInteraction, + itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in + var title: String? + if group.id == AnyHashable("recent") { + //TODO:localize + title = "Recently Used".uppercased() + } else { + for (id, info, _) in view.collectionInfos { + if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo { + title = info.title.uppercased() + break + } + } + } + + return EmojiPagerContentComponent.ItemGroup(id: group.id, title: title, items: group.items) + }, + itemLayoutType: .compact + ) + } + + let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + |> distinctUntilChanged + + let stickerItems: Signal = combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000), + hasPremium + ) + |> map { view, hasPremium -> EmojiPagerContentComponent in + struct ItemGroup { + var id: AnyHashable + var items: [EmojiPagerContentComponent.Item] + } + var itemGroups: [ItemGroup] = [] + var itemGroupIndexById: [AnyHashable: Int] = [:] + + var savedStickers: OrderedItemListView? + var recentStickers: OrderedItemListView? + var premiumStickers: OrderedItemListView? + var cloudPremiumStickers: OrderedItemListView? + for orderedView in view.orderedItemListsViews { + if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers { + recentStickers = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers { + savedStickers = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.PremiumStickers { + premiumStickers = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudPremiumStickers { + cloudPremiumStickers = orderedView + } + } + + if let savedStickers = savedStickers { + for item in savedStickers.items { + guard let item = item.contents.get(SavedStickerItem.self) else { + continue + } + if isPremiumDisabled && item.file.isPremiumSticker { + continue + } + + let resultItem = EmojiPagerContentComponent.Item( + emoji: "", + file: item.file + ) + + let groupId = "saved" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) + } + } + } + + if let recentStickers = recentStickers { + var count = 0 + for item in recentStickers.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + if isPremiumDisabled && item.media.isPremiumSticker { + continue + } + + let resultItem = EmojiPagerContentComponent.Item( + emoji: "", + file: item.media + ) + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) + } + + count += 1 + if count >= 5 { + break + } + } + } + + var hasPremiumStickers = false + if hasPremium { + if let premiumStickers = premiumStickers, !premiumStickers.items.isEmpty { + hasPremiumStickers = true + } else if let cloudPremiumStickers = cloudPremiumStickers, !cloudPremiumStickers.items.isEmpty { + hasPremiumStickers = true + } + } + + if hasPremiumStickers { + var premiumStickers = premiumStickers?.items ?? [] + if let cloudPremiumStickers = cloudPremiumStickers { + premiumStickers.append(contentsOf: cloudPremiumStickers.items) + } + + var processedIds = Set() + for item in premiumStickers { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + if isPremiumDisabled && item.media.isPremiumSticker { + continue + } + if processedIds.contains(item.media.fileId) { + continue + } + processedIds.insert(item.media.fileId) + + let resultItem = EmojiPagerContentComponent.Item( + emoji: "", + file: item.media + ) + + let groupId = "premium" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) + } + } + } + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + let resultItem = EmojiPagerContentComponent.Item( + emoji: "", + file: item.file + ) + let groupId = entry.index.collectionId + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) + } + } + + return EmojiPagerContentComponent( + context: context, + animationCache: animationCache, + animationRenderer: animationRenderer, + inputInteraction: stickerInputInteraction, + itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in + var title: String? + if group.id == AnyHashable("saved") { + title = nil + } else if group.id == AnyHashable("recent") { + //TODO:localize + title = "Recently Used".uppercased() + } else if group.id == AnyHashable("premium") { + //TODO:localize + title = "Premium".uppercased() + } else { + for (id, info, _) in view.collectionInfos { + if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo { + title = info.title.uppercased() + break + } + } + } + + return EmojiPagerContentComponent.ItemGroup(id: group.id, title: title, items: group.items) + }, + itemLayoutType: .detailed + ) + } + + let gifItems: Signal = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) + |> map { savedGifs -> GifPagerContentComponent in + var items: [GifPagerContentComponent.Item] = [] + for gifItem in savedGifs { + items.append(GifPagerContentComponent.Item( + file: gifItem.contents.get(RecentMediaItem.self)!.media + )) + } + return GifPagerContentComponent( + context: context, + inputInteraction: gifInputInteraction, + items: items + ) + } + + return combineLatest(queue: .mainQueue(), + emojiItems, + stickerItems, + gifItems + ) + |> map { emoji, stickers, gifs -> InputData in + return InputData( + emoji: emoji, + stickers: stickers, + gifs: gifs + ) + } + } + + private let entityKeyboardView: ComponentHostView + + private var currentInputData: InputData + private var inputDataDisposable: Disposable? + + init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal) { + self.currentInputData = currentInputData + self.entityKeyboardView = ComponentHostView() + + super.init() + + self.view.addSubview(self.entityKeyboardView) + + self.externalTopPanelContainer = SparseContainerView() + + self.inputDataDisposable = (updatedInputData + |> deliverOnMainQueue).start(next: { [weak self] inputData in + guard let strongSelf = self else { + return + } + strongSelf.currentInputData = inputData + }) + } + + deinit { + self.inputDataDisposable?.dispose() + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) { + let entityKeyboardSize = self.entityKeyboardView.update( + transition: Transition(transition), + component: AnyComponent(EntityKeyboardComponent( + theme: interfaceState.theme, + bottomInset: bottomInset, + emojiContent: self.currentInputData.emoji, + stickerContent: self.currentInputData.stickers, + gifContent: self.currentInputData.gifs, + externalTopPanelContainer: self.externalTopPanelContainer, + topPanelExtensionUpdated: { [weak self] topPanelExtension, transition in + guard let strongSelf = self else { + return + } + if strongSelf.topBackgroundExtension != topPanelExtension { + strongSelf.topBackgroundExtension = topPanelExtension + strongSelf.topBackgroundExtensionUpdated?(transition.containedViewLayoutTransition) + } + } + )), + environment: {}, + containerSize: CGSize(width: width, height: standardInputHeight) + ) + transition.updateFrame(view: self.entityKeyboardView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: entityKeyboardSize)) + + return (standardInputHeight, 0.0) + } +} diff --git a/submodules/TelegramUI/Sources/ChatInputNode.swift b/submodules/TelegramUI/Sources/ChatInputNode.swift index 0b955aecc1..d365208a9f 100644 --- a/submodules/TelegramUI/Sources/ChatInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatInputNode.swift @@ -11,6 +11,11 @@ class ChatInputNode: ASDisplayNode { return .single(Void()) } + var externalTopPanelContainer: UIView? + + var topBackgroundExtension: CGFloat = 41.0 + var topBackgroundExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)? + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) { return (0.0, 0.0) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 2e95f80d98..68d107097c 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -317,13 +317,9 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte if isTextEmpty && chatPresentationInterfaceState.hasBots && chatPresentationInterfaceState.hasBotCommands { accessoryItems.append(.commands) } - #if DEBUG + accessoryItems.append(.stickers(stickersEnabled)) - #else - if isTextEmpty { - accessoryItems.append(.stickers(stickersEnabled)) - } - #endif + if isTextEmpty, let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup, chatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != message.id { accessoryItems.append(.inputButtons) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift index 71007c4caa..031c020cfa 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift @@ -6,12 +6,24 @@ import Postbox import AccountContext import ChatPresentationInterfaceState -func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, inputMediaNode: ChatMediaInputNode?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?) -> ChatInputNode? { +func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, inputMediaNode: ChatMediaInputNode?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?, makeMediaInputNode: () -> ChatInputNode?) -> ChatInputNode? { if !(inputPanelNode is ChatTextInputPanelNode) { return nil } switch chatPresentationInterfaceState.inputMode { - case .media: + case .media: + if context.sharedContext.immediateExperimentalUISettings.inlineStickers { + if let currentNode = currentNode as? ChatEntityKeyboardInputNode { + return currentNode + } else if let inputMediaNode = inputMediaNode { + return inputMediaNode + } else if let inputMediaNode = makeMediaInputNode() { + inputMediaNode.interfaceInteraction = interfaceInteraction + return inputMediaNode + } else { + return nil + } + } else { if let currentNode = currentNode as? ChatMediaInputNode { return currentNode } else if let inputMediaNode = inputMediaNode { @@ -39,19 +51,20 @@ func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: inputNode.interfaceInteraction = interfaceInteraction return inputNode } - case .inputButtons: - if chatPresentationInterfaceState.forceInputCommandsHidden { - return nil - } else { - if let currentNode = currentNode as? ChatButtonKeyboardInputNode { - return currentNode - } else { - let inputNode = ChatButtonKeyboardInputNode(context: context, controllerInteraction: controllerInteraction) - inputNode.interfaceInteraction = interfaceInteraction - return inputNode - } - } - case .none, .text: + } + case .inputButtons: + if chatPresentationInterfaceState.forceInputCommandsHidden { return nil + } else { + if let currentNode = currentNode as? ChatButtonKeyboardInputNode { + return currentNode + } else { + let inputNode = ChatButtonKeyboardInputNode(context: context, controllerInteraction: controllerInteraction) + inputNode.interfaceInteraction = interfaceInteraction + return inputNode + } + } + case .none, .text: + return nil } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift index 3468153e01..288c9d53ba 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift @@ -238,7 +238,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { if let (collection, result) = file.contextResult { let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) } else { - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, false) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, false) } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index e50820d4b6..f6cdf73996 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -796,7 +796,7 @@ final class ChatMediaInputNode: ChatInputNode { sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -864,7 +864,7 @@ final class ChatMediaInputNode: ChatInputNode { sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -1096,7 +1096,7 @@ final class ChatMediaInputNode: ChatInputNode { let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) let controller = StickerPackScreen(context: strongSelf.context, updatedPresentationData: strongSelf.controllerInteraction.updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -1467,7 +1467,7 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in f(.default) if isSaved { - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, false) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, false) } else if let (collection, result) = file.contextResult { let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) } @@ -1486,7 +1486,7 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in f(.default) if isSaved { - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, true, false) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, true, false) } else if let (collection, result) = file.contextResult { let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, true) } @@ -1499,7 +1499,7 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in f(.default) - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, true) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, true) }))) } } @@ -1608,9 +1608,9 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode, animationNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode, imageNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -1622,9 +1622,9 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode, animationNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode, imageNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -1678,7 +1678,7 @@ final class ChatMediaInputNode: ChatInputNode { if let packReference = packReference { let controller = StickerPackScreen(context: strongSelf.context, updatedPresentationData: strongSelf.controllerInteraction.updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -1760,9 +1760,9 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode, animationNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode, imageNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -1774,9 +1774,9 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode, animationNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode, imageNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -1832,7 +1832,7 @@ final class ChatMediaInputNode: ChatInputNode { if let packReference = packReference { let controller = StickerPackScreen(context: strongSelf.context, updatedPresentationData: strongSelf.controllerInteraction.updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift index f06f403ee9..8c9fb1451f 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift @@ -412,7 +412,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state { if let isLocked = self.isLocked, isLocked { } else { - let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, false, nil, false, self, self.bounds) + let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, false, nil, false, self.view, self.bounds, nil) self.imageNode.layer.animateAlpha(from: 0.5, to: 1.0, duration: 1.0) } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift index 2f102ab512..1b0375f792 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift @@ -326,7 +326,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index f6fbf62026..c11adf3f34 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -2319,18 +2319,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let localSourceContentFrame = CGRect( origin: CGPoint( - x: self.imageNode.frame.minX + self.imageNode.frame.size.width / 2.0 - stickerSource.imageNode.frame.size.width / 2.0, - y: self.imageNode.frame.minY + self.imageNode.frame.size.height / 2.0 - stickerSource.imageNode.frame.size.height / 2.0 + x: self.imageNode.frame.minX + self.imageNode.frame.size.width / 2.0 - stickerSource.sourceFrame.size.width / 2.0, + y: self.imageNode.frame.minY + self.imageNode.frame.size.height / 2.0 - stickerSource.sourceFrame.size.height / 2.0 ), - size: stickerSource.imageNode.frame.size + size: stickerSource.sourceFrame.size ) - var snapshotView: UIView? - if let animationNode = stickerSource.animationNode { - snapshotView = animationNode.view.snapshotContentTree() - } else { - snapshotView = stickerSource.imageNode.view.snapshotContentTree() - } + let snapshotView: UIView? = stickerSource.snapshotContentTree() snapshotView?.frame = localSourceContentFrame if let snapshotView = snapshotView { @@ -2350,7 +2345,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { y: localSourceCenter.y - localSourceContentFrame.height / 2.0 ) - let sourceScale: CGFloat = stickerSource.imageNode.frame.height / self.imageNode.frame.height + let sourceScale: CGFloat = stickerSource.sourceFrame.height / self.imageNode.frame.height let offset = CGPoint( x: sourceCenter.x - self.imageNode.frame.midX, @@ -2393,8 +2388,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { animationNode.layer.animateAlpha(from: 0.0, to: animationNode.alpha, duration: 0.4) } - stickerSource.imageNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - stickerSource.imageNode.layer.animateAlpha(from: 0.0, to: stickerSource.imageNode.alpha, duration: 0.4) + if let sourceLayer = stickerSource.sourceLayer { + sourceLayer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + sourceLayer.animateAlpha(from: 0.0, to: CGFloat(sourceLayer.opacity), duration: 0.4) + } if let placeholderNode = stickerSource.placeholderNode { placeholderNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index d7657cb0fd..2f57d3b0a4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -1487,17 +1487,17 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let localSourceContentFrame = CGRect( origin: CGPoint( - x: self.imageNode.frame.minX + self.imageNode.frame.size.width / 2.0 - stickerSource.imageNode.frame.size.width / 2.0, - y: self.imageNode.frame.minY + self.imageNode.frame.size.height / 2.0 - stickerSource.imageNode.frame.size.height / 2.0 + x: self.imageNode.frame.minX + self.imageNode.frame.size.width / 2.0 - stickerSource.sourceFrame.size.width / 2.0, + y: self.imageNode.frame.minY + self.imageNode.frame.size.height / 2.0 - stickerSource.sourceFrame.size.height / 2.0 ), - size: stickerSource.imageNode.frame.size + size: stickerSource.sourceFrame.size ) var snapshotView: UIView? if let animationNode = stickerSource.animationNode { snapshotView = animationNode.view.snapshotContentTree() } else { - snapshotView = stickerSource.imageNode.view.snapshotContentTree() + snapshotView = stickerSource.snapshotContentTree() } snapshotView?.frame = localSourceContentFrame @@ -1518,7 +1518,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { y: localSourceCenter.y - localSourceContentFrame.height / 2.0 ) - let sourceScale: CGFloat = stickerSource.imageNode.frame.height / self.imageNode.frame.height + let sourceScale: CGFloat = stickerSource.sourceFrame.height / self.imageNode.frame.height let offset = CGPoint( x: sourceCenter.x - self.imageNode.frame.midX, @@ -1554,8 +1554,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView { animationNode.layer.animateAlpha(from: 0.0, to: animationNode.alpha, duration: 0.4) } - stickerSource.imageNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - stickerSource.imageNode.layer.animateAlpha(from: 0.0, to: stickerSource.imageNode.alpha, duration: 0.4) + if let sourceLayer = stickerSource.sourceLayer { + sourceLayer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + sourceLayer.animateAlpha(from: 0.0, to: CGFloat(sourceLayer.opacity), duration: 0.4) + } if let placeholderNode = stickerSource.placeholderNode { placeholderNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 4bd4d9df65..b66b0bb07a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -47,18 +47,18 @@ private final class CachedChatMessageText { } private final class InlineStickerItem: Hashable { - let file: TelegramMediaFile + let emoji: ChatTextInputTextCustomEmojiAttribute - init(file: TelegramMediaFile) { - self.file = file + init(emoji: ChatTextInputTextCustomEmojiAttribute) { + self.emoji = emoji } func hash(into hasher: inout Hasher) { - hasher.combine(self.file.fileId) + hasher.combine(emoji.fileId) } static func ==(lhs: InlineStickerItem, rhs: InlineStickerItem) -> Bool { - if lhs.file.fileId != rhs.file.fileId { + if lhs.emoji.fileId != rhs.emoji.fileId { return false } return true @@ -341,38 +341,26 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { attributedText = NSAttributedString(string: " ", font: textFont, textColor: messageTheme.primaryTextColor) } - /*if let entities = entities { + if let entities = entities { let updatedString = NSMutableAttributedString(attributedString: attributedText) for entity in entities.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { - guard case .AnimatedEmoji = entity.type else { + guard case let .CustomEmoji(stickerPack, fileId) = entity.type else { continue } let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) - let substring = updatedString.attributedSubstring(from: range) + let 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(emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId)) - let emoji = substring.string.basicEmoji.0 - - var emojiFile: TelegramMediaFile? - emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file - if emojiFile == nil { - emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file - } - - if let emojiFile = emojiFile { - let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil) - var updatedAttributes: [NSAttributedString.Key: Any] = currentDict - updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor - updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(file: emojiFile) - - let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}]", attributes: updatedAttributes) - updatedString.replaceCharacters(in: range, with: insertString) - } + let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}\u{00a0}]", attributes: updatedAttributes) + updatedString.replaceCharacters(in: range, with: insertString) } attributedText = updatedString - }*/ + } let cutout: TextNodeCutout? = nil @@ -558,27 +546,27 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) { - var nextIndexById: [MediaId: Int] = [:] + var nextIndexById: [Int64: Int] = [:] var validIds: [InlineStickerItemLayer.Key] = [] if let textLayout = textLayout { for item in textLayout.embeddedItems { if let stickerItem = item.value as? InlineStickerItem { let index: Int - if let currentNext = nextIndexById[stickerItem.file.fileId] { + if let currentNext = nextIndexById[stickerItem.emoji.fileId] { index = currentNext } else { index = 0 } - nextIndexById[stickerItem.file.fileId] = index + 1 - let id = InlineStickerItemLayer.Key(id: stickerItem.file.fileId, index: index) + nextIndexById[stickerItem.emoji.fileId] = index + 1 + let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index) validIds.append(id) let itemLayer: InlineStickerItemLayer if let current = self.inlineStickerItemLayers[id] { itemLayer = current } else { - itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor) + itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, emoji: stickerItem.emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor) self.inlineStickerItemLayers[id] = itemLayer self.textNode.layer.addSublayer(itemLayer) itemLayer.isVisibleForAnimations = self.isVisibleForAnimations diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index dfa894b6d0..bf079a44e7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -111,17 +111,51 @@ public final class ChatMessageTransitionNode: ASDisplayNode { } final class Sticker { - let imageNode: TransformImageNode + let imageNode: TransformImageNode? let animationNode: AnimatedStickerNode? let placeholderNode: ASDisplayNode? + let imageLayer: CALayer? let relativeSourceRect: CGRect + + var sourceFrame: CGRect { + if let imageNode = self.imageNode { + return imageNode.frame + } else if let imageLayer = self.imageLayer { + return imageLayer.bounds + } else { + return CGRect(origin: CGPoint(), size: relativeSourceRect.size) + } + } + + var sourceLayer: CALayer? { + if let imageNode = self.imageNode { + return imageNode.layer + } else if let imageLayer = self.imageLayer { + return imageLayer + } else { + return nil + } + } - init(imageNode: TransformImageNode, animationNode: AnimatedStickerNode?, placeholderNode: ASDisplayNode?, relativeSourceRect: CGRect) { + init(imageNode: TransformImageNode?, animationNode: AnimatedStickerNode?, placeholderNode: ASDisplayNode?, imageLayer: CALayer?, relativeSourceRect: CGRect) { self.imageNode = imageNode self.animationNode = animationNode self.placeholderNode = placeholderNode + self.imageLayer = imageLayer self.relativeSourceRect = relativeSourceRect } + + func snapshotContentTree() -> UIView? { + if let animationNode = self.animationNode { + return animationNode.view.snapshotContentTree() + } else if let imageNode = self.imageNode { + return imageNode.view.snapshotContentTree() + } else if let sourceLayer = self.imageLayer { + return sourceLayer.snapshotContentTreeAsView() + } else { + return nil + } + } } enum Source { @@ -142,6 +176,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode { enum StickerInput { case inputPanel(itemNode: ChatMediaInputStickerGridItemNode) case mediaPanel(itemNode: HorizontalStickerGridItemNode) + case universal(sourceContainerView: UIView, sourceRect: CGRect, sourceLayer: CALayer) case inputPanelSearch(itemNode: StickerPaneSearchStickerItemNode) case emptyPanel(itemNode: ChatEmptyNodeStickerContentNode) } @@ -388,16 +423,19 @@ public final class ChatMessageTransitionNode: ASDisplayNode { let sourceAbsoluteRect: CGRect switch stickerMediaInput { case let .inputPanel(sourceItemNode): - stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, relativeSourceRect: sourceItemNode.imageNode.frame) - sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view) + stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, imageLayer: nil, relativeSourceRect: sourceItemNode.imageNode.frame) + sourceAbsoluteRect = sourceItemNode.view.convert(sourceItemNode.imageNode.frame, to: self.view) case let .mediaPanel(sourceItemNode): - stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, relativeSourceRect: sourceItemNode.imageNode.frame) - sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view) + stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, imageLayer: nil, relativeSourceRect: sourceItemNode.imageNode.frame) + sourceAbsoluteRect = sourceItemNode.view.convert(sourceItemNode.imageNode.frame, to: self.view) + case let .universal(sourceContainerView, sourceRect, sourceLayer): + stickerSource = Sticker(imageNode: nil, animationNode: nil, placeholderNode: nil, imageLayer: sourceLayer, relativeSourceRect: sourceLayer.frame) + sourceAbsoluteRect = sourceContainerView.convert(sourceRect, to: self.view) case let .inputPanelSearch(sourceItemNode): - stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: nil, relativeSourceRect: sourceItemNode.imageNode.frame) - sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view) + stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: nil, imageLayer: nil, relativeSourceRect: sourceItemNode.imageNode.frame) + sourceAbsoluteRect = sourceItemNode.view.convert(sourceItemNode.imageNode.frame, to: self.view) case let .emptyPanel(sourceItemNode): - stickerSource = Sticker(imageNode: sourceItemNode.stickerNode.imageNode, animationNode: sourceItemNode.stickerNode.animationNode, placeholderNode: nil, relativeSourceRect: sourceItemNode.stickerNode.imageNode.frame) + stickerSource = Sticker(imageNode: sourceItemNode.stickerNode.imageNode, animationNode: sourceItemNode.stickerNode.animationNode, placeholderNode: nil, imageLayer: nil, relativeSourceRect: sourceItemNode.stickerNode.imageNode.frame) sourceAbsoluteRect = sourceItemNode.stickerNode.view.convert(sourceItemNode.stickerNode.imageNode.frame, to: self.view) } @@ -442,7 +480,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode { self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true) switch stickerMediaInput { - case .inputPanel: + case .inputPanel, .universal: break case let .mediaPanel(sourceItemNode): sourceItemNode.isHidden = true diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift index eaa4b84307..b700e2524d 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift @@ -105,7 +105,7 @@ final class ChatRecentActionsController: TelegramBaseController { }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { - }, sendSticker: { _, _, _, _ in + }, sendSticker: { _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in @@ -153,6 +153,8 @@ final class ChatRecentActionsController: TelegramBaseController { }, displayCopyProtectionTip: { _, _ in }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in + }, insertText: { _ in + }, backwardsDeleteText: { }, chatController: { return nil }, statuses: nil) diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index fa72a7a018..936cb29704 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -261,7 +261,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in + }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in self?.openUrl(url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 06ddecf35f..59a51c5c83 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -177,7 +177,7 @@ private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPres return result } -private func calculateTextFieldRealInsets(_ presentationInterfaceState: ChatPresentationInterfaceState) -> UIEdgeInsets { +private func calculateTextFieldRealInsets(presentationInterfaceState: ChatPresentationInterfaceState, accessoryButtonsWidth: CGFloat) -> UIEdgeInsets { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) let top: CGFloat let bottom: CGFloat @@ -194,7 +194,11 @@ private func calculateTextFieldRealInsets(_ presentationInterfaceState: ChatPres top = 0.0 bottom = 0.0 } - return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: 0.0) + + var right: CGFloat = 0.0 + right += accessoryButtonsWidth + + return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: right) } private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, UIImage)? @@ -240,6 +244,67 @@ enum ChatTextInputPanelPasteData { case sticker(UIImage, Bool) } +final class CustomEmojiContainerView: UIView { + private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView? + + private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:] + + init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) { + self.emojiViewProvider = emojiViewProvider + + super.init(frame: CGRect()) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) { + var nextIndexById: [Int64: Int] = [:] + + var validKeys = Set() + for (rect, emoji) in emojiRects { + let index: Int + if let nextIndex = nextIndexById[emoji.fileId] { + index = nextIndex + } else { + index = 0 + } + nextIndexById[emoji.fileId] = index + 1 + + let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index) + + let view: UIView + if let current = self.emojiLayers[key] { + view = current + } else if let newView = self.emojiViewProvider(emoji) { + view = newView + self.addSubview(newView) + self.emojiLayers[key] = view + } else { + continue + } + + let size = CGSize(width: 24.0, height: 24.0) + + view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0)), size: size) + + validKeys.insert(key) + } + + var removeKeys: [InlineStickerItemLayer.Key] = [] + for (key, view) in self.emojiLayers { + if !validKeys.contains(key) { + removeKeys.append(key) + view.removeFromSuperview() + } + } + for key in removeKeys { + self.emojiLayers.removeValue(forKey: key) + } + } +} + class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let clippingNode: ASDisplayNode var textPlaceholderNode: ImmediateTextNode @@ -249,6 +314,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textInputContainer: ASDisplayNode var textInputNode: EditableTextNode? var dustNode: InvisibleInkDustNode? + var customEmojiContainerView: CustomEmojiContainerView? let textInputBackgroundNode: ASImageNode private var transparentTextInputBackgroundImage: UIImage? @@ -459,7 +525,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private var touchDownGestureRecognizer: TouchDownGestureRecognizer? - private var emojiViewProvider: ((String) -> UIView)? + private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? init(presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { self.presentationInterfaceState = presentationInterfaceState @@ -667,15 +733,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputBackgroundNode.isUserInteractionEnabled = true self.textInputBackgroundNode.view.addGestureRecognizer(recognizer) - /*if let presentationContext = presentationContext { + if let presentationContext = presentationContext { self.emojiViewProvider = { [weak self, weak presentationContext] emoji in - guard let strongSelf = self, let presentationContext = presentationContext, let presentationInterfaceState = strongSelf.presentationInterfaceState, let context = strongSelf.context, let file = strongSelf.context?.animatedEmojiStickers[emoji]?.first?.file else { + guard let strongSelf = self, let presentationContext = presentationContext, let presentationInterfaceState = strongSelf.presentationInterfaceState, let context = strongSelf.context else { return UIView() } - return EmojiTextAttachmentView(context: context, file: file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12)) + return EmojiTextAttachmentView(context: context, emoji: emoji, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12)) } - }*/ + } } required init?(coder aDecoder: NSCoder) { @@ -725,26 +791,26 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true self.textInputNode = textInputNode + var accessoryButtonsWidth: CGFloat = 0.0 + var firstButton = true + for (_, button) in self.accessoryItemButtons { + if firstButton { + firstButton = false + accessoryButtonsWidth += accessoryButtonInset + } else { + accessoryButtonsWidth += accessoryButtonSpacing + } + accessoryButtonsWidth += button.buttonWidth + } + if let presentationInterfaceState = self.presentationInterfaceState { refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) - textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState) + textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) } if !self.textInputContainer.bounds.size.width.isZero { let textInputFrame = self.textInputContainer.frame - var accessoryButtonsWidth: CGFloat = 0.0 - var firstButton = true - for (_, button) in self.accessoryItemButtons { - if firstButton { - firstButton = false - accessoryButtonsWidth += accessoryButtonInset - } else { - accessoryButtonsWidth += accessoryButtonSpacing - } - accessoryButtonsWidth += button.buttonWidth - } - textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) textInputNode.view.layoutIfNeeded() self.updateSpoiler() @@ -1647,7 +1713,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var textInputViewRealInsets = UIEdgeInsets() if let presentationInterfaceState = self.presentationInterfaceState { - textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState) + textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) } let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) @@ -1717,7 +1783,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.slowmodePlaceholderNode?.isHidden = true } - var nextButtonTopRight = CGPoint(x: width - rightInset - textFieldInsets.right - accessoryButtonInset, y: minimalHeight - textFieldInsets.bottom - minimalInputHeight) + var nextButtonTopRight = CGPoint(x: width - rightInset - textFieldInsets.right - accessoryButtonInset, y: panelHeight - textFieldInsets.bottom - minimalInputHeight) for (_, button) in self.accessoryItemButtons.reversed() { let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight) button.updateLayout(size: buttonSize) @@ -1900,6 +1966,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor var rects: [CGRect] = [] + var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = [] if let attributedText = textInputNode.attributedText { let beginning = textInputNode.textView.beginningOfDocument @@ -1936,6 +2003,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let endIndex = currentIndex addSpoiler(startIndex: currentStartIndex, endIndex: endIndex) } + } else if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { + if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) { + let textRects = textInputNode.textView.selectionRects(for: textRange) + for textRect in textRects { + customEmojiRects.append((textRect.rect, value)) + break + } + } } }) } @@ -1957,6 +2032,28 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { dustNode.removeFromSupernode() self.dustNode = nil } + + if !customEmojiRects.isEmpty { + let customEmojiContainerView: CustomEmojiContainerView + if let current = self.customEmojiContainerView { + customEmojiContainerView = current + } else { + customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in + guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else { + return nil + } + return emojiViewProvider(emoji) + }) + customEmojiContainerView.isUserInteractionEnabled = false + textInputNode.textView.addSubview(customEmojiContainerView) + self.customEmojiContainerView = customEmojiContainerView + } + + customEmojiContainerView.update(emojiRects: customEmojiRects) + } else if let customEmojiContainerView = self.customEmojiContainerView { + customEmojiContainerView.removeFromSuperview() + self.customEmojiContainerView = nil + } } private func updateSpoilersRevealed(animated: Bool = true) { @@ -2222,7 +2319,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - if mediaInputIsActive { + if mediaInputIsActive && !"".isEmpty { if self.actionButtons.expandMediaInputButton.alpha.isZero { self.actionButtons.expandMediaInputButton.alpha = 1.0 if animated { @@ -2320,9 +2417,23 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { - self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in - return (.text, state.keyboardButtonsMessage?.id) - }) + guard let interfaceInteraction = self.interfaceInteraction, let presentationInterfaceState = self.presentationInterfaceState else { + return + } + + switch presentationInterfaceState.inputMode { + case .text: + break + case .media: + break + case .inputButtons, .none: + if self.textInputNode?.textView.inputView == nil { + interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.text, state.keyboardButtonsMessage?.id) + }) + } + } + self.inputMenu.activate() } @@ -2330,9 +2441,20 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage self.inputMenu.deactivate() - if let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil { - self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { _ in - return (.inputButtons, nil) + if let presentationInterfaceState = self.presentationInterfaceState { + if let peer = presentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil, presentationInterfaceState.keyboardButtonsMessage != nil { + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { _ in + return (.inputButtons, nil) + } + } else { + switch presentationInterfaceState.inputMode { + case .text, .media: + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { _ in + return (.none, nil) + } + default: + break + } } } } @@ -2698,6 +2820,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputNode?.becomeFirstResponder() } + func backwardsDeleteText() { + guard let textInputNode = self.textInputNode else { + return + } + textInputNode.textView.deleteBackward() + } + @objc func expandButtonPressed() { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in if case let .media(mode, expanded, focused) = state.inputMode { diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index dc168430ca..13daf942a7 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -40,7 +40,7 @@ private struct DrawingPaneArrangement { private final class DrawingStickersScreenNode: ViewControllerTracingNode { private let context: AccountContext private var presentationData: PresentationData - fileprivate var selectSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + fileprivate var selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private var searchItemContext = StickerPaneSearchGlobalItemContext() private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> @@ -98,7 +98,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { fileprivate var dismiss: (() -> Void)? - init(context: AccountContext, selectSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) { + init(context: AccountContext, selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) { self.context = context let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData @@ -106,13 +106,13 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) - var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + var selectStickerImpl: ((FileMediaReference, UIView, CGRect) -> Bool)? self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect, _ in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: { @@ -236,7 +236,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -349,7 +349,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -1306,7 +1306,7 @@ final class DrawingStickersScreen: ViewController, TGPhotoPaintStickersScreen { public var screenWillDisappear: (() -> Void)? private let context: AccountContext - var selectSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + var selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private var controllerNode: DrawingStickersScreenNode { return self.displayNode as! DrawingStickersScreenNode @@ -1322,7 +1322,7 @@ final class DrawingStickersScreen: ViewController, TGPhotoPaintStickersScreen { return self._ready } - public init(context: AccountContext, selectSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil) { + public init(context: AccountContext, selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil) { self.context = context self.selectSticker = selectSticker @@ -1362,10 +1362,10 @@ final class DrawingStickersScreen: ViewController, TGPhotoPaintStickersScreen { override public func loadDisplayNode() { self.displayNode = DrawingStickersScreenNode( context: self.context, - selectSticker: { [weak self] file, sourceNode, sourceRect in + selectSticker: { [weak self] file, sourceView, sourceRect in if let strongSelf = self, let selectSticker = strongSelf.selectSticker { (strongSelf.displayNode as! DrawingStickersScreenNode).animateOut() - return selectSticker(file, sourceNode, sourceRect) + return selectSticker(file, sourceView, sourceRect) } else { return false } diff --git a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift index 39986aa3ae..2c5e72a1c4 100644 --- a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift +++ b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift @@ -192,7 +192,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { private let context: AccountContext private var presentationData: PresentationData private weak var controller: FeaturedStickersScreen? - private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private var searchItemContext = StickerPaneSearchGlobalItemContext() let gridNode: GridNode @@ -222,7 +222,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } private var didSetReady: Bool = false - init(context: AccountContext, controller: FeaturedStickersScreen, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) { + init(context: AccountContext, controller: FeaturedStickersScreen, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.controller = controller @@ -485,9 +485,9 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode, animationNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode, imageNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode.view, imageNode.bounds) } } f(.default) @@ -576,7 +576,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { menuItems = [ .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController, let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode, animationNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds) } f(.default) })), @@ -789,7 +789,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { final class FeaturedStickersScreen: ViewController { private let context: AccountContext fileprivate let highlightedPackId: ItemCollectionId? - private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private var controllerNode: FeaturedStickersScreenNode { return self.displayNode as! FeaturedStickersScreenNode @@ -805,7 +805,7 @@ final class FeaturedStickersScreen: ViewController { fileprivate var searchNavigationNode: SearchNavigationContentNode? - public init(context: AccountContext, highlightedPackId: ItemCollectionId?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil) { + public init(context: AccountContext, highlightedPackId: ItemCollectionId?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil) { self.context = context self.highlightedPackId = highlightedPackId self.sendSticker = sendSticker @@ -1034,7 +1034,7 @@ private enum FeaturedSearchEntry: Identifiable, Comparable { switch self { case let .sticker(_, code, stickerItem, theme): return StickerPaneSearchStickerItem(account: account, code: code, stickerItem: stickerItem, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { node, rect in - interaction.sendSticker(.standalone(media: stickerItem.file), node, rect) + interaction.sendSticker(.standalone(media: stickerItem.file), node.view, rect) }) case let .global(_, info, topItems, installed, topSeparator): return StickerPaneSearchGlobalItem(account: account, theme: theme, strings: strings, listAppearance: true, fillsRow: true, info: info, topItems: topItems, topSeparator: topSeparator, regularInsets: false, installed: installed, unread: false, open: { @@ -1080,7 +1080,7 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { private let inputNodeInteraction: ChatMediaInputNodeInteraction private var interaction: StickerPaneSearchInteraction? private weak var controller: FeaturedStickersScreen? - private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let itemContext: StickerPaneSearchGlobalItemContext private var theme: PresentationTheme @@ -1115,7 +1115,7 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { } var isActiveUpdated: (() -> Void)? - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, inputNodeInteraction: ChatMediaInputNodeInteraction, controller: FeaturedStickersScreen, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, itemContext: StickerPaneSearchGlobalItemContext) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, inputNodeInteraction: ChatMediaInputNodeInteraction, controller: FeaturedStickersScreen, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, itemContext: StickerPaneSearchGlobalItemContext) { self.context = context self.inputNodeInteraction = inputNodeInteraction self.controller = controller @@ -1175,9 +1175,9 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { |> deliverOnMainQueue).start(next: { _ in }) } - }, sendSticker: { [weak self] file, sourceNode, sourceRect in + }, sendSticker: { [weak self] file, sourceView, sourceRect in if let strongSelf = self { - let _ = strongSelf.sendSticker?(file, sourceNode, sourceRect) + let _ = strongSelf.sendSticker?(file, sourceView, sourceRect) } }, getItemIsPreviewed: { item in return inputNodeInteraction.previewedStickerPackItem == .pack(item) diff --git a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift index dc2993be47..6e55f0cbb4 100644 --- a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift @@ -333,7 +333,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { if let (collection, result) = file.contextResult { let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) } else { - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, false) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, false) } } diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift index 23a4da53d9..a705670b73 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -162,9 +162,9 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont f(.default) if let strongSelf = self { - let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.interfaceInteraction?.getNavigationController(), sendSticker: { file, sourceNode, sourceRect in + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.interfaceInteraction?.getNavigationController(), sendSticker: { file, sourceView, sourceRect in if let strongSelf = self { - return strongSelf.interfaceInteraction?.sendSticker(file, false, sourceNode, sourceRect) ?? false + return strongSelf.interfaceInteraction?.sendSticker(file, false, sourceView, sourceRect, nil) ?? false } else { return false } diff --git a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift index 479356050c..5385b98998 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift @@ -17,11 +17,11 @@ final class HorizontalStickerGridItem: GridItem { let file: TelegramMediaFile let theme: PresentationTheme let isPreviewed: (HorizontalStickerGridItem) -> Bool - let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Void + let sendSticker: (FileMediaReference, UIView, CGRect) -> Void let section: GridSection? = nil - init(account: Account, file: TelegramMediaFile, theme: PresentationTheme, isPreviewed: @escaping (HorizontalStickerGridItem) -> Bool, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Void) { + init(account: Account, file: TelegramMediaFile, theme: PresentationTheme, isPreviewed: @escaping (HorizontalStickerGridItem) -> Bool, sendSticker: @escaping (FileMediaReference, UIView, CGRect) -> Void) { self.account = account self.file = file self.theme = theme @@ -54,7 +54,7 @@ final class HorizontalStickerGridItemNode: GridItemNode { private let stickerFetchedDisposable = MetaDisposable() - var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)? + var sendSticker: ((FileMediaReference, UIView, CGRect) -> Void)? private var currentIsPreviewing: Bool = false @@ -238,7 +238,7 @@ final class HorizontalStickerGridItemNode: GridItemNode { @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { if let (_, item, _) = self.currentState, case .ended = recognizer.state { - self.sendSticker?(.standalone(media: item.file), self, self.bounds) + self.sendSticker?(.standalone(media: item.file), self.view, self.bounds) } } diff --git a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift index 4cbfdca401..930cdacc64 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift @@ -72,7 +72,7 @@ private struct StickerEntry: Identifiable, Comparable { return HorizontalStickerGridItem(account: account, file: self.file, theme: theme, isPreviewed: { item in return false//stickersInteraction.previewedStickerItem == item }, sendSticker: { file, node, rect in - let _ = interfaceInteraction.sendSticker(file, true, node, rect) + let _ = interfaceInteraction.sendSticker(file, true, node, rect, nil) }) } } @@ -180,7 +180,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode, itemNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode.view, itemNode.bounds, nil) })), .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) @@ -224,7 +224,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { if let packReference = packReference { let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect) + return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect, nil) } else { return false } diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index 74f9b3eca9..9a0280d1eb 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -45,7 +45,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie var previewedStickerItem: StickerPackItem? var updateBackgroundOffset: ((CGFloat, Bool, ContainedViewLayoutTransition) -> Void)? - var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)? + var sendSticker: ((FileMediaReference, UIView, CGRect) -> Void)? var getControllerInteraction: (() -> ChatControllerInteraction?)? @@ -111,9 +111,9 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, animationNode, animationNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, imageNode, imageNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -125,9 +125,9 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, animationNode, animationNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, imageNode, imageNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -179,7 +179,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie if let packReference = packReference { let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self, let controllerInteraction = strongSelf.getControllerInteraction?() { - return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect) + return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect, nil) } else { return false } @@ -434,8 +434,8 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie theme: self.theme, isPreviewed: { [weak self] item in return item.file.fileId == self?.previewedStickerItem?.file.fileId - }, sendSticker: { [weak self] file, node, rect in - self?.sendSticker?(file, node, rect) + }, sendSticker: { [weak self] file, view, rect in + self?.sendSticker?(file, view, rect) } ) itemNode = item.node(layout: GridNodeLayout( @@ -576,7 +576,7 @@ final class InlineReactionSearchPanel: ChatInputContextPanelNode { guard let strongSelf = self else { return } - let _ = strongSelf.controllerInteraction?.sendSticker(file, false, false, strongSelf.query, true, node, rect) + let _ = strongSelf.controllerInteraction?.sendSticker(file, false, false, strongSelf.query, true, node, rect, nil) } self.view.disablesInteractiveTransitionGestureRecognizer = true diff --git a/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift b/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift index 6ae0117529..a5bd2aa3c8 100644 --- a/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift @@ -120,7 +120,7 @@ func legacyInputMicPalette(from theme: PresentationTheme) -> TGModernConversatio return TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: theme.rootController.navigationBar.opaqueBackgroundColor, borderColor: inputPanelTheme.panelSeparatorColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor) } -func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, context: AccountContext, peerId: PeerId, slowmodeState: ChatSlowmodeState?, hasSchedule: Bool, send: @escaping (InstantVideoController, EnqueueMessage?) -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void) -> InstantVideoController { +func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, context: AccountContext, peerId: PeerId, slowmodeState: ChatSlowmodeState?, hasSchedule: Bool, send: @escaping (InstantVideoController, EnqueueMessage?) -> Void, displaySlowmodeTooltip: @escaping (UIView, CGRect) -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void) -> InstantVideoController { let isSecretChat = peerId.namespace == Namespaces.Peer.SecretChat let legacyController = InstantVideoController(presentation: .custom, theme: theme) @@ -244,7 +244,7 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, controller.displaySlowmodeTooltip = { [weak legacyController, weak controller] in if let legacyController = legacyController, let controller = controller { let rect = controller.frameForSendButton() - displaySlowmodeTooltip(legacyController.displayNode, rect) + displaySlowmodeTooltip(legacyController.displayNode.view, rect) } } legacyController.bindCaptureController(controller) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 490c7bc021..cd1c1377ac 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -45,7 +45,7 @@ private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatContr } } -func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)? = nil, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) { +func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)? = nil, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) { let updatedPresentationData: (initial: PresentationData, signal: Signal)? if case let .chat(_, maybeUpdatedPresentationData) = urlContext { updatedPresentationData = maybeUpdatedPresentationData diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 2029e31d4c..5c6dd1e3b0 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -80,7 +80,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in - }, sendSticker: { _, _, _, _, _, _, _ in + }, sendSticker: { _, _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 9b550ad332..ccc3a9f4c8 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -310,7 +310,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { - }, sendSticker: { _, _, _, _ in + }, sendSticker: { _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in @@ -359,6 +359,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { displayCopyProtectionTip(node, save) }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in + }, insertText: { _ in + }, backwardsDeleteText: { }, chatController: { return nil }, statuses: nil) @@ -2208,7 +2210,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) }, sendCurrentMessage: { _ in }, sendMessage: { _ in - }, sendSticker: { _, _, _, _, _, _, _ in + }, sendSticker: { _, _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false @@ -3357,8 +3359,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate transitionCompletion() }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.controller?.present(controller, in: .window(.root)) @@ -6062,10 +6064,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let paintStickersContext = LegacyPaintStickersContext(context: strongSelf.context) paintStickersContext.presentStickersController = { completion in - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in let coder = PostboxEncoder() coder.encodeRootObject(fileReference.media) - completion?(coder.makeData(), fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + completion?(coder.makeData(), fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.controller?.present(controller, in: .window(.root)) diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index cb19f498c2..6d71ab7ba3 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -252,7 +252,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { - }, sendSticker: { _, _, _, _ in + }, sendSticker: { _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in @@ -349,6 +349,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, displayCopyProtectionTip: { _, _ in }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in + }, insertText: { _ in + }, backwardsDeleteText: { }, chatController: { return nil }, statuses: nil) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 066c92a5bd..7f8e089a21 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1201,7 +1201,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return resolveUrlImpl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) } - public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) { + public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) { openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, forceExternal: forceExternal, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, requestMessageActionUrlAuth: requestMessageActionUrlAuth, joinVoiceChat: joinVoiceChat, present: present, dismissInput: dismissInput, contentContext: contentContext) } @@ -1280,7 +1280,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { tapMessage?(message) }, clickThroughMessage: { clickThroughMessage?() - }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in + }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in diff --git a/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift index 3f735b3a4c..92ea1c0b39 100644 --- a/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift @@ -19,10 +19,10 @@ import UndoUI final class StickerPaneSearchInteraction { let open: (StickerPackCollectionInfo) -> Void let install: (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void - let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Void + let sendSticker: (FileMediaReference, UIView, CGRect) -> Void let getItemIsPreviewed: (StickerPackItem) -> Bool - init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { + init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void, sendSticker: @escaping (FileMediaReference, UIView, CGRect) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { self.open = open self.install = install self.sendSticker = sendSticker @@ -100,7 +100,7 @@ private enum StickerSearchEntry: Identifiable, Comparable { switch self { case let .sticker(_, code, stickerItem, theme): return StickerPaneSearchStickerItem(account: account, code: code, stickerItem: stickerItem, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { node, rect in - interaction.sendSticker(.standalone(media: stickerItem.file), node, rect) + interaction.sendSticker(.standalone(media: stickerItem.file), node.view, rect) }) case let .global(_, info, topItems, installed, topSeparator): let itemContext = StickerPaneSearchGlobalItemContext() @@ -226,7 +226,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { [weak self] fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -319,9 +319,9 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { |> deliverOnMainQueue).start(next: { _ in }) } - }, sendSticker: { [weak self] file, sourceNode, sourceRect in + }, sendSticker: { [weak self] file, sourceView, sourceRect in if let strongSelf = self { - let _ = strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect) + let _ = strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) } }, getItemIsPreviewed: { item in return inputNodeInteraction.previewedStickerPackItem == .pack(item) diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift index a0055a4545..d313b5cb28 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift @@ -126,7 +126,7 @@ final class StickersChatInputContextPanelItemNode: ListViewItemNode { for i in 0 ..< self.nodes.count { if self.nodes[i].frame.contains(location) { let file = item.files[i] - let _ = item.interfaceInteraction.sendSticker(.standalone(media: file), true, self.nodes[i], self.nodes[i].bounds) + let _ = item.interfaceInteraction.sendSticker(.standalone(media: file), true, self.nodes[i].view, self.nodes[i].bounds, nil) break } } diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift index b31262461c..f72a055c94 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift @@ -136,7 +136,7 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode, itemNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode.view, itemNode.bounds, nil) })), .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) @@ -180,7 +180,7 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { if let packReference = packReference { let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect) + return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect, nil) } else { return false } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index f9dbd1d6e7..3bdae8b476 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -3,6 +3,7 @@ import UIKit import Display import AsyncDisplayKit import Postbox +import TelegramCore import TelegramPresentationData import Emoji @@ -17,8 +18,9 @@ public struct ChatTextInputAttributes { public static let textMention = NSAttributedString.Key(rawValue: "Attribute__TextMention") public static let textUrl = NSAttributedString.Key(rawValue: "Attribute__TextUrl") public static let spoiler = NSAttributedString.Key(rawValue: "Attribute__Spoiler") + public static let customEmoji = NSAttributedString.Key(rawValue: "Attribute__CustomEmoji") - public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler] + public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler, ChatTextInputAttributes.customEmoji] } public func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttributedString { @@ -28,7 +30,7 @@ public func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttrib 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)) + sourceString.replaceCharacters(in: range, with: NSAttributedString(string: value.text, attributes: [ChatTextInputAttributes.customEmoji: value.emoji])) stop.pointee = true found = true } @@ -64,7 +66,7 @@ public struct ChatTextFontAttributes: OptionSet { public static let blockQuote = ChatTextFontAttributes(rawValue: 1 << 3) } -public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((String) -> UIView)?) -> NSAttributedString { +public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) -> NSAttributedString { let result = NSMutableAttributedString(string: stateText.string) let fullRange = NSRange(location: 0, length: result.length) @@ -108,6 +110,8 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo } else { result.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) } + } else if key == ChatTextInputAttributes.customEmoji { + result.addAttribute(key, value: value, range: range) } } @@ -215,6 +219,26 @@ public final class ChatTextInputTextUrlAttribute: NSObject { } } +public final class ChatTextInputTextCustomEmojiAttribute: NSObject { + public let stickerPack: StickerPackReference + public let fileId: Int64 + + public init(stickerPack: StickerPackReference, fileId: Int64) { + self.stickerPack = stickerPack + self.fileId = fileId + + super.init() + } + + override public func isEqual(_ object: Any?) -> Bool { + if let other = object as? ChatTextInputTextCustomEmojiAttribute { + return self.stickerPack == other.stickerPack && self.fileId == other.fileId + } else { + return false + } + } +} + private func textUrlRangesEqual(_ lhs: [(NSRange, ChatTextInputTextUrlAttribute)], _ rhs: [(NSRange, ChatTextInputTextUrlAttribute)]) -> Bool { if lhs.count != rhs.count { return false @@ -459,7 +483,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed } } -public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((String) -> UIView)?) { +public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) { guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else { return } @@ -493,10 +517,14 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.textMention, range: fullRange) textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.textUrl, range: fullRange) textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.spoiler, range: fullRange) + textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.customEmoji, range: fullRange) textNode.textView.textStorage.addAttribute(NSAttributedString.Key.font, value: Font.regular(baseFontSize), range: fullRange) textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.chat.inputPanel.primaryTextColor, range: fullRange) + let replaceRanges: [(NSRange, EmojiTextAttachment)] = [] + + //var emojiIndex = 0 attributedText.enumerateAttributes(in: fullRange, options: [], using: { attributes, range, _ in var fontAttributes: ChatTextFontAttributes = [] @@ -530,6 +558,17 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme } else { textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) } + } else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { + textNode.textView.textStorage.addAttribute(key, value: value, range: range) + if let emojiViewProvider = emojiViewProvider { + let _ = emojiViewProvider + /*let emojiText = attributedText.attributedSubstring(from: range) + let attachment = EmojiTextAttachment(index: emojiIndex, text: emojiText.string, emoji: value, viewProvider: emojiViewProvider) + emojiIndex += 1 + attachment.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 16.0)) + + replaceRanges.append((range, attachment))*/ + } } } @@ -556,12 +595,16 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme } } }) + + for (range, attachment) in replaceRanges.sorted(by: { $0.0.location > $1.0.location }) { + textNode.textView.textStorage.replaceCharacters(in: range, with: NSAttributedString(attachment: attachment)) + } } - if #available(iOS 15, *), let emojiViewProvider = emojiViewProvider { + if #available(iOS 15, *), let _ = emojiViewProvider { let _ = CustomTextAttachmentViewProvider.ensureRegistered - var nextIndex: [String: Int] = [:] + /*var nextIndex: [String: Int] = [:] var count = 0 @@ -572,7 +615,7 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme } }) - while count < 10 { + while count < 400 { 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 { @@ -601,11 +644,11 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme if !found { break } - } + }*/ } } -public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set, emojiViewProvider: ((String) -> UIView)?, spoilersRevealed: Bool = false) { +public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, spoilersRevealed: Bool = false) { guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else { return } @@ -902,10 +945,12 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu } private final class EmojiTextAttachment: NSTextAttachment { - let emoji: String - let viewProvider: (String) -> UIView + let text: String + let emoji: ChatTextInputTextCustomEmojiAttribute + let viewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView - init(index: Int, emoji: String, viewProvider: @escaping (String) -> UIView) { + init(index: Int, text: String, emoji: ChatTextInputTextCustomEmojiAttribute, viewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView) { + self.text = text self.emoji = emoji self.viewProvider = viewProvider diff --git a/submodules/TextFormat/Sources/GenerateTextEntities.swift b/submodules/TextFormat/Sources/GenerateTextEntities.swift index dbaa90b7a6..35efbe4f81 100644 --- a/submodules/TextFormat/Sources/GenerateTextEntities.swift +++ b/submodules/TextFormat/Sources/GenerateTextEntities.swift @@ -145,26 +145,6 @@ private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimatedEmojisInText: Int? = nil) -> [MessageTextEntity] { var entities: [MessageTextEntity] = [] - - if let maxAnimatedEmojisInText = maxAnimatedEmojisInText, maxAnimatedEmojisInText != 0 { - 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(nil))) - - count += 1 - if count >= maxAnimatedEmojisInText { - stop = true - } - } - } - }) - } text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: [], using: { attributes, range, _ in for (key, value) in attributes { @@ -184,6 +164,8 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .TextUrl(url: value.url))) } else if key == ChatTextInputAttributes.spoiler { entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Spoiler)) + } else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .CustomEmoji(stickerPack: value.stickerPack, fileId: value.fileId))) } } }) diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index afde2592a1..2d6ebff158 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -40,6 +40,8 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M string.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: range) case .Spoiler: string.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: range) + case let .CustomEmoji(stickerPack, fileId): + string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId), range: range) default: break }