diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 645a17fd58..f83c91637c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -13346,9 +13346,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return updated }) - let _ = (self.cachedDataPromise.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] cachedData in + let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) + |> map { animatedEmoji -> [String: [StickerPackItem]] in + var animatedEmojiStickers: [String: [StickerPackItem]] = [:] + switch animatedEmoji { + case let .result(_, items, _): + for case let item as StickerPackItem in items { + if let emoji = item.getStringRepresentationsOfIndexKeys().first { + animatedEmojiStickers[emoji.basicEmoji.0] = [item] + let strippedEmoji = emoji.basicEmoji.0.strippedEmoji + if animatedEmojiStickers[strippedEmoji] == nil { + animatedEmojiStickers[strippedEmoji] = [item] + } + } + } + default: + break + } + return animatedEmojiStickers + } + + let _ = (combineLatest(queue: Queue.mainQueue(), self.cachedDataPromise.get(), animatedEmojiStickers) + |> take(1)).start(next: { [weak self] cachedData, animatedEmojiStickers in guard let strongSelf = self else { return } @@ -13364,7 +13383,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G selectedEmoticon = nil } - let controller = ChatThemeScreen(context: context, updatedPresentationData: strongSelf.updatedPresentationData, initiallySelectedEmoticon: selectedEmoticon, previewTheme: { [weak self] emoticon, dark in + let controller = ChatThemeScreen(context: context, updatedPresentationData: strongSelf.updatedPresentationData, animatedEmojiStickers: animatedEmojiStickers, initiallySelectedEmoticon: selectedEmoticon, previewTheme: { [weak self] emoticon, dark in if let strongSelf = self { strongSelf.presentCrossfadeSnapshot(delay: 0.2) strongSelf.themeEmoticonAndDarkAppearancePreviewPromise.set(.single((emoticon, dark))) diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index 0a176ad40c..cda2f0ade3 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -14,8 +14,12 @@ import PresentationDataUtils import AnimationUI import MergeLists import MediaResources +import StickerResources import WallpaperResources import TooltipUI +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import ShimmerEffect private func closeButtonImage(theme: PresentationTheme) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in @@ -41,6 +45,7 @@ private func closeButtonImage(theme: PresentationTheme) -> UIImage? { private struct ThemeSettingsThemeEntry: Comparable, Identifiable { let index: Int let emoticon: String? + let emojiFile: TelegramMediaFile? let themeReference: PresentationThemeReference? var selected: Bool let theme: PresentationTheme @@ -58,6 +63,7 @@ private struct ThemeSettingsThemeEntry: Comparable, Identifiable { if lhs.emoticon != rhs.emoticon { return false } + if lhs.themeReference?.index != rhs.themeReference?.index { return false } @@ -81,7 +87,7 @@ private struct ThemeSettingsThemeEntry: Comparable, Identifiable { } func item(context: AccountContext, action: @escaping (String?) -> Void) -> ListViewItem { - return ThemeSettingsThemeIconItem(context: context, emoticon: self.emoticon, themeReference: self.themeReference, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action) + return ThemeSettingsThemeIconItem(context: context, emoticon: self.emoticon, emojiFile: self.emojiFile, themeReference: self.themeReference, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action) } } @@ -89,6 +95,7 @@ private struct ThemeSettingsThemeEntry: Comparable, Identifiable { private class ThemeSettingsThemeIconItem: ListViewItem { let context: AccountContext let emoticon: String? + let emojiFile: TelegramMediaFile? let themeReference: PresentationThemeReference? let selected: Bool let theme: PresentationTheme @@ -96,9 +103,10 @@ private class ThemeSettingsThemeIconItem: ListViewItem { let wallpaper: TelegramWallpaper? let action: (String?) -> Void - public init(context: AccountContext, emoticon: String?, themeReference: PresentationThemeReference?, selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (String?) -> Void) { + public init(context: AccountContext, emoticon: String?, emojiFile: TelegramMediaFile?, themeReference: PresentationThemeReference?, selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (String?) -> Void) { self.context = context self.emoticon = emoticon + self.emojiFile = emojiFile self.themeReference = themeReference self.selected = selected self.theme = theme @@ -235,9 +243,28 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { private let overlayNode: ASImageNode private let textNode: TextNode private let emojiNode: TextNode + private let emojiImageNode: TransformImageNode + private var animatedStickerNode: AnimatedStickerNode? + private var placeholderNode: StickerShimmerEffectNode var snapshotView: UIView? var item: ThemeSettingsThemeIconItem? + + override var visibility: ListViewItemNodeVisibility { + didSet { + self.visibilityStatus = self.visibility != .none + } + } + + private var visibilityStatus: Bool = false { + didSet { + if self.visibilityStatus != oldValue { + self.animatedStickerNode?.visibility = self.visibilityStatus + } + } + } + + private let stickerFetchedDisposable = MetaDisposable() init() { self.containerNode = ASDisplayNode() @@ -257,6 +284,10 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { self.emojiNode = TextNode() self.emojiNode.isUserInteractionEnabled = false + + self.emojiImageNode = TransformImageNode() + + self.placeholderNode = StickerShimmerEffectNode() super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) @@ -265,8 +296,43 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { self.containerNode.addSubnode(self.overlayNode) self.containerNode.addSubnode(self.textNode) self.containerNode.addSubnode(self.emojiNode) + self.containerNode.addSubnode(self.placeholderNode) + + var firstTime = true + self.emojiImageNode.imageUpdated = { [weak self] image in + guard let strongSelf = self else { + return + } + if image != nil { + strongSelf.removePlaceholder(animated: !firstTime) + if firstTime { + strongSelf.emojiImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + firstTime = false + } } + deinit { + self.stickerFetchedDisposable.dispose() + } + + private func removePlaceholder(animated: Bool) { + if !animated { + self.placeholderNode.removeFromSupernode() + } else { + self.placeholderNode.alpha = 0.0 + self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.placeholderNode.removeFromSupernode() + }) + } + } + + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + let emojiFrame = CGRect(origin: CGPoint(x: 33.0, y: 79.0), size: CGSize(width: 24.0, height: 24.0)) + self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + emojiFrame.minX, y: rect.minY + emojiFrame.minY), size: emojiFrame.size), within: containerSize) + } + func asyncLayout() -> (ThemeSettingsThemeIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeEmojiLayout = TextNode.asyncLayout(self.emojiNode) @@ -296,7 +362,13 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { let text = NSAttributedString(string: item.strings.Conversation_Theme_NoTheme, font: Font.semibold(15.0), textColor: item.theme.actionSheet.controlAccentColor) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let title = NSAttributedString(string: item.emoticon ?? "❌", font: Font.regular(22.0), textColor: .black) + var emoticon = item.emoticon + if emoticon == "🦁" { + emoticon = "🌳" + } else if emoticon == "🔮" { + emoticon = "🎆" + } + let title = NSAttributedString(string: emoticon != nil ? "" : "❌", font: Font.regular(22.0), textColor: .black) let (_, emojiApply) = makeEmojiLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 120.0, height: 90.0), insets: UIEdgeInsets()) @@ -334,6 +406,39 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { strongSelf.overlayNode.frame = strongSelf.imageNode.frame.insetBy(dx: -1.0, dy: -1.0) strongSelf.emojiNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 79.0), size: CGSize(width: 90.0, height: 30.0)) + + let emojiFrame = CGRect(origin: CGPoint(x: 33.0, y: 79.0), size: CGSize(width: 24.0, height: 24.0)) + if let file = item.emojiFile { + let imageApply = strongSelf.emojiImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: emojiFrame.size, boundingSize: emojiFrame.size, intrinsicInsets: UIEdgeInsets())) + imageApply() + strongSelf.emojiImageNode.setSignal(chatMessageStickerPackThumbnail(postbox: item.context.account.postbox, resource: file.resource, animated: true, nilIfEmpty: true)) + strongSelf.emojiImageNode.frame = emojiFrame + + let animatedStickerNode: AnimatedStickerNode + if let current = strongSelf.animatedStickerNode { + animatedStickerNode = current + } else { + animatedStickerNode = AnimatedStickerNode() + animatedStickerNode.started = { [weak self] in + self?.emojiImageNode.isHidden = true + } + strongSelf.animatedStickerNode = animatedStickerNode + strongSelf.containerNode.insertSubnode(animatedStickerNode, belowSubnode: strongSelf.placeholderNode) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 128, height: 128, mode: .cached) + } + animatedStickerNode.visibility = strongSelf.visibilityStatus + + strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + + let thumbnailDimensions = PixelDimensions(width: 512, height: 512) + strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, imageSize: thumbnailDimensions.cgSize) + strongSelf.placeholderNode.frame = emojiFrame + } + + if let animatedStickerNode = strongSelf.animatedStickerNode { + animatedStickerNode.frame = emojiFrame + animatedStickerNode.updateLayout(size: emojiFrame.size) + } } }) } @@ -376,6 +481,7 @@ final class ChatThemeScreen: ViewController { private var animatedIn = false private let context: AccountContext + private let animatedEmojiStickers: [String: [StickerPackItem]] private let initiallySelectedEmoticon: String? private let dismissByTapOutside: Bool private let previewTheme: (String?, Bool?) -> Void @@ -392,9 +498,10 @@ final class ChatThemeScreen: ViewController { } } - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), initiallySelectedEmoticon: String?, dismissByTapOutside: Bool = true, previewTheme: @escaping (String?, Bool?) -> Void, completion: @escaping (String?) -> Void) { + init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), animatedEmojiStickers: [String: [StickerPackItem]], initiallySelectedEmoticon: String?, dismissByTapOutside: Bool = true, previewTheme: @escaping (String?, Bool?) -> Void, completion: @escaping (String?) -> Void) { self.context = context self.presentationData = updatedPresentationData.initial + self.animatedEmojiStickers = animatedEmojiStickers self.initiallySelectedEmoticon = initiallySelectedEmoticon self.dismissByTapOutside = dismissByTapOutside self.previewTheme = previewTheme @@ -426,7 +533,7 @@ final class ChatThemeScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = ChatThemeScreenNode(context: self.context, presentationData: self.presentationData, initiallySelectedEmoticon: self.initiallySelectedEmoticon, dismissByTapOutside: self.dismissByTapOutside) + self.displayNode = ChatThemeScreenNode(context: self.context, presentationData: self.presentationData, animatedEmojiStickers: self.animatedEmojiStickers, initiallySelectedEmoticon: self.initiallySelectedEmoticon, dismissByTapOutside: self.dismissByTapOutside) self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl self.controllerNode.previewTheme = { [weak self] emoticon, dark in guard let strongSelf = self else { @@ -558,7 +665,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega var dismiss: (() -> Void)? var cancel: (() -> Void)? - init(context: AccountContext, presentationData: PresentationData, initiallySelectedEmoticon: String?, dismissByTapOutside: Bool) { + init(context: AccountContext, presentationData: PresentationData, animatedEmojiStickers: [String: [StickerPackItem]], initiallySelectedEmoticon: String?, dismissByTapOutside: Bool) { self.context = context self.initiallySelectedEmoticon = initiallySelectedEmoticon self.selectedEmoticon = initiallySelectedEmoticon @@ -662,10 +769,16 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega var entries: [ThemeSettingsThemeEntry] = [] if strongSelf.initiallySelectedEmoticon != nil { - entries.append(ThemeSettingsThemeEntry(index: 0, emoticon: nil, themeReference: nil, selected: selectedEmoticon == nil, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) + entries.append(ThemeSettingsThemeEntry(index: 0, emoticon: nil, emojiFile: nil, themeReference: nil, selected: selectedEmoticon == nil, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) } for theme in themes { - entries.append(ThemeSettingsThemeEntry(index: entries.count, emoticon: theme.emoji, themeReference: .cloud(PresentationCloudTheme(theme: isDarkAppearance ? theme.darkTheme : theme.theme, resolvedWallpaper: nil, creatorAccountId: nil)), selected: selectedEmoticon == theme.emoji, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) + var emoticon = theme.emoji + if emoticon == "🦁" { + emoticon = "🌳" + } else if emoticon == "🔮" { + emoticon = "🎆" + } + entries.append(ThemeSettingsThemeEntry(index: entries.count, emoticon: theme.emoji, emojiFile: animatedEmojiStickers[emoticon]?.first?.file, themeReference: .cloud(PresentationCloudTheme(theme: isDarkAppearance ? theme.darkTheme : theme.theme, resolvedWallpaper: nil, creatorAccountId: nil)), selected: selectedEmoticon == theme.emoji, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) } let action: (String?) -> Void = { [weak self] emoticon in