diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index fb2e3f1932..1a338b0ca4 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -71,10 +71,13 @@ swift_library( "//submodules/PremiumUI:PremiumUI", "//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent", "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/EmojiStatusSelectionComponent", + "//submodules/TelegramUI/Components/EntityKeyboard", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 07b889783b..a5ec3537d5 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -30,6 +30,10 @@ import ComponentFlow import LottieAnimationComponent import ProgressIndicatorComponent import PremiumUI +import AnimationCache +import MultiAnimationRenderer +import EmojiStatusSelectionComponent +import EntityKeyboard private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { if listNode.scroller.isDragging { @@ -109,6 +113,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private let controlsHistoryPreload: Bool private let hideNetworkActivityStatus: Bool + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + public let groupId: PeerGroupId public let filter: ChatListFilter? public let previewing: Bool @@ -182,7 +189,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.presentationData = (context.sharedContext.currentPresentationData.with { $0 }) self.presentationDataValue.set(.single(self.presentationData)) - self.titleView = ChatListTitleView(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + + self.titleView = ChatListTitleView( + context: context, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer + ) self.tabContainerNode = ChatListFilterTabContainerNode() @@ -201,9 +219,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController title = self.presentationData.strings.ChatList_ArchivedChatsTitle } - self.titleView.title = NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false) + self.titleView.title = NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: nil) self.navigationItem.titleView = self.titleView + self.titleView.openStatusSetup = { [weak self] sourceView in + self?.openStatusSetup(sourceView: sourceView) + } + if !previewing { if self.groupId == .root && self.filter == nil { self.tabBarItem.title = self.presentationData.strings.DialogList_Title @@ -304,6 +326,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return (data.isLockable, false) } + let peerStatus: Signal = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> NetworkStatusTitle.Status? in + guard case let .user(user) = peer else { + return nil + } + if let emojiStatus = user.emojiStatus { + return .emoji(emojiStatus) + } else if user.isPremium { + return .premium + } else { + return nil + } + } + |> distinctUntilChanged + let previousEditingAndNetworkStateValue = Atomic<(Bool, AccountNetworkState)?>(value: nil) if !self.hideNetworkActivityStatus { self.titleDisposable = combineLatest(queue: .mainQueue(), @@ -311,8 +348,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController hasProxy, passcode, self.chatListDisplayNode.containerNode.currentItemState, - self.isReorderingTabsValue.get() - ).start(next: { [weak self] networkState, proxy, passcode, stateAndFilterId, isReorderingTabs in + self.isReorderingTabsValue.get(), + peerStatus + ).start(next: { [weak self] networkState, proxy, passcode, stateAndFilterId, isReorderingTabs, peerStatus in if let strongSelf = self { let defaultTitle: String if strongSelf.groupId == .root { @@ -333,7 +371,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController animated = true } } - strongSelf.titleView.setTitle(NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false), animated: animated) + strongSelf.titleView.setTitle(NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus), animated: animated) } else if isReorderingTabs { if strongSelf.groupId == .root { strongSelf.navigationItem.setRightBarButton(nil, animated: true) @@ -344,17 +382,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let (_, connectsViaProxy) = proxy switch networkState { case .waitingForNetwork: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) case let .connecting(proxy): var text = strongSelf.presentationData.strings.State_Connecting if let layout = strongSelf.validLayout, proxy != nil && layout.metrics.widthClass != .regular && layout.size.width > 320.0 { text = strongSelf.presentationData.strings.State_ConnectingToProxy } - strongSelf.titleView.title = NetworkStatusTitle(text: text, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false) + strongSelf.titleView.title = NetworkStatusTitle(text: text, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) case .updating: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) case .online: - strongSelf.titleView.title = NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false) + strongSelf.titleView.title = NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) } } else { var isRoot = false @@ -401,7 +439,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController var checkProxy = false switch networkState { case .waitingForNetwork: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) case let .connecting(proxy): var text = strongSelf.presentationData.strings.State_Connecting if let layout = strongSelf.validLayout, proxy != nil && layout.metrics.widthClass != .regular && layout.size.width > 320.0 { @@ -410,11 +448,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let proxy = proxy, proxy.hasConnectionIssues { checkProxy = true } - strongSelf.titleView.title = NetworkStatusTitle(text: text, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked) + strongSelf.titleView.title = NetworkStatusTitle(text: text, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) case .updating: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) case .online: - strongSelf.titleView.setTitle(NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked), animated: (previousEditingAndNetworkState?.0 ?? false) != stateAndFilterId.state.editing) + strongSelf.titleView.setTitle(NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus), animated: (previousEditingAndNetworkState?.0 ?? false) != stateAndFilterId.state.editing) } if groupId == .root && filter == nil && checkProxy { if strongSelf.proxyUnavailableTooltipController == nil && !strongSelf.didShowProxyUnavailableTooltipController && strongSelf.isNodeLoaded && strongSelf.displayNode.view.window != nil && strongSelf.navigationController?.topViewController === self { @@ -804,6 +842,25 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.activeDownloadsDisposable?.dispose() } + private func openStatusSetup(sourceView: UIView) { + self.present(EmojiStatusSelectionController( + context: self.context, + sourceView: sourceView, + emojiContent: EmojiPagerContentComponent.emojiInputData( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + isStandalone: false, + isStatusSelection: true, + isReactionSelection: false, + reactionItems: [], + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: self.context.account.peerId + ) + ), in: .window(.root)) + } + private func updateThemeAndStrings() { if case .root = self.groupId { self.tabBarItem.title = self.presentationData.strings.DialogList_Title @@ -858,7 +915,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } override public func loadDisplayNode() { - self.displayNode = ChatListControllerNode(context: self.context, groupId: EngineChatList.Group(self.groupId), filter: self.filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, controller: self) + self.displayNode = ChatListControllerNode(context: self.context, groupId: EngineChatList.Group(self.groupId), filter: self.filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, controller: self) self.chatListDisplayNode.navigationBar = self.navigationBar diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index fe6b03ac48..82602e6a98 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -11,6 +11,8 @@ import AccountContext import SearchBarNode import SearchUI import ContextUI +import AnimationCache +import MultiAnimationRenderer enum ChatListContainerNodeFilter: Equatable { case all @@ -171,7 +173,7 @@ private final class ChatListShimmerNode: ASDisplayNode { self.addSubnode(self.maskNode) } - func update(context: AccountContext, size: CGSize, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + func update(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, size: CGSize, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData { self.currentParams = (size, presentationData) @@ -180,7 +182,7 @@ private final class ChatListShimmerNode: ASDisplayNode { let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil)) let timestamp1: Int32 = 100000 let peers: [EnginePeer.Id: EnginePeer] = [:] - let interaction = ChatListNodeInteraction(context: context, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() }, present: { _ in }) @@ -279,6 +281,8 @@ private final class ChatListShimmerNode: ASDisplayNode { private final class ChatListContainerItemNode: ASDisplayNode { private let context: AccountContext + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private var presentationData: PresentationData private let becameEmpty: (ChatListFilter?) -> Void private let emptyAction: (ChatListFilter?) -> Void @@ -292,13 +296,15 @@ private final class ChatListContainerItemNode: ASDisplayNode { private var validLayout: (CGSize, UIEdgeInsets, CGFloat)? - init(context: AccountContext, groupId: EngineChatList.Group, filter: ChatListFilter?, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void) { + init(context: AccountContext, groupId: EngineChatList.Group, filter: ChatListFilter?, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void) { self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.presentationData = presentationData self.becameEmpty = becameEmpty self.emptyAction = emptyAction - self.listNode = ChatListNode(context: context, groupId: groupId, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: .chatList, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) + self.listNode = ChatListNode(context: context, groupId: groupId, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: .chatList, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true) super.init() @@ -384,7 +390,7 @@ private final class ChatListContainerItemNode: ASDisplayNode { } private func layoutEmptyShimmerEffectNode(node: ChatListShimmerNode, size: CGSize, insets: UIEdgeInsets, verticalOffset: CGFloat, transition: ContainedViewLayoutTransition) { - node.update(context: self.context, size: size, presentationData: self.presentationData, transition: .immediate) + node.update(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, size: size, presentationData: self.presentationData, transition: .immediate) transition.updateFrameAdditive(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: size)) } @@ -430,6 +436,9 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private var presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + private var itemNodes: [ChatListFilterTabEntryId: ChatListContainerItemNode] = [:] private var pendingItemNode: (ChatListFilterTabEntryId, ChatListContainerItemNode, Disposable)? private(set) var availableFilters: [ChatListContainerNodeFilter] = [.all] @@ -582,7 +591,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { var didBeginSelectingChats: (() -> Void)? var displayFilterLimit: (() -> Void)? - init(context: AccountContext, groupId: EngineChatList.Group, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, filterBecameEmpty: @escaping (ChatListFilter?) -> Void, filterEmptyAction: @escaping (ChatListFilter?) -> Void) { + init(context: AccountContext, groupId: EngineChatList.Group, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, filterBecameEmpty: @escaping (ChatListFilter?) -> Void, filterEmptyAction: @escaping (ChatListFilter?) -> Void) { self.context = context self.groupId = groupId self.previewing = previewing @@ -591,12 +600,14 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.controlsHistoryPreload = controlsHistoryPreload self.presentationData = presentationData + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.selectedId = .all super.init() - let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: nil, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: presentationData, becameEmpty: { [weak self] filter in + let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: nil, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, becameEmpty: { [weak self] filter in self?.filterBecameEmpty(filter) }, emptyAction: { [weak self] filter in self?.filterEmptyAction(filter) @@ -872,7 +883,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { itemNode.emptyNode?.restartAnimation() completion?() } else if self.pendingItemNode == nil { - let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: self.availableFilters[index].filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, becameEmpty: { [weak self] filter in + let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: self.availableFilters[index].filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, becameEmpty: { [weak self] filter in self?.filterBecameEmpty(filter) }, emptyAction: { [weak self] filter in self?.filterEmptyAction(filter) @@ -1000,7 +1011,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { validNodeIds.append(id) if self.itemNodes[id] == nil && self.enableAdjacentFilterLoading && !self.disableItemNodeOperationsWhileAnimating { - let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: self.availableFilters[i].filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, becameEmpty: { [weak self] filter in + let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: self.availableFilters[i].filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, becameEmpty: { [weak self] filter in self?.filterBecameEmpty(filter) }, emptyAction: { [weak self] filter in self?.filterEmptyAction(filter) @@ -1064,6 +1075,8 @@ final class ChatListControllerNode: ASDisplayNode { private let context: AccountContext private let groupId: EngineChatList.Group private var presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer let containerNode: ChatListContainerNode let inlineTabContainerNode: ChatListFilterTabInlineContainerNode @@ -1096,14 +1109,16 @@ final class ChatListControllerNode: ASDisplayNode { let debugListView = ListView() - init(context: AccountContext, groupId: EngineChatList.Group, filter: ChatListFilter?, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, controller: ChatListControllerImpl) { + init(context: AccountContext, groupId: EngineChatList.Group, filter: ChatListFilter?, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, controller: ChatListControllerImpl) { self.context = context self.groupId = groupId self.presentationData = presentationData + self.animationCache = animationCache + self.animationRenderer = animationRenderer var filterBecameEmpty: ((ChatListFilter?) -> Void)? var filterEmptyAction: ((ChatListFilter?) -> Void)? - self.containerNode = ChatListContainerNode(context: context, groupId: groupId, previewing: previewing, controlsHistoryPreload: controlsHistoryPreload, presentationData: presentationData, filterBecameEmpty: { filter in + self.containerNode = ChatListContainerNode(context: context, groupId: groupId, previewing: previewing, controlsHistoryPreload: controlsHistoryPreload, presentationData: presentationData, animationCache: animationCache, animationRenderer: animationRenderer, filterBecameEmpty: { filter in filterBecameEmpty?(filter) }, filterEmptyAction: { filter in filterEmptyAction?(filter) @@ -1246,7 +1261,7 @@ final class ChatListControllerNode: ASDisplayNode { let filter: ChatListNodePeersFilter = [] - let contentNode = ChatListSearchContainerNode(context: self.context, filter: filter, groupId: self.groupId, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, dismissSearch in + let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, groupId: self.groupId, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, dismissSearch in self?.requestOpenPeerFromSearch?(peer, dismissSearch) }, openDisabledPeer: { _ in }, openRecentPeerOptions: { [weak self] peer in diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 420d380b1f..77b40bd16a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -31,6 +31,8 @@ import UndoUI import TextFormat import Postbox import TelegramAnimatedStickerNode +import AnimationCache +import MultiAnimationRenderer private enum ChatListTokenId: Int32 { case archive @@ -129,7 +131,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var validLayout: (ContainerViewLayout, CGFloat)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter, groupId: EngineChatList.Group, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) { + public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter, groupId: EngineChatList.Group, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) { self.context = context self.peersFilter = filter self.groupId = groupId @@ -146,7 +148,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.presentInGlobalOverlay = presentInGlobalOverlay self.filterContainerNode = ChatListSearchFiltersContainerNode() - self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, groupId: groupId, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) + self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, groupId: groupId, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) self.paneContainerNode.clipsToBounds = true super.init() diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 4cf88d94d2..91f5a322f9 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -28,6 +28,8 @@ import ChatListSearchRecentPeersNode import UndoUI import Postbox import FetchManagerImpl +import AnimationCache +import MultiAnimationRenderer private enum ChatListRecentEntryStableId: Hashable { case topPeers @@ -822,6 +824,8 @@ private struct DownloadItem: Equatable { final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let context: AccountContext + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let interaction: ChatListSearchInteraction private let peersFilter: ChatListNodePeersFilter private var presentationData: PresentationData @@ -893,8 +897,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var hiddenMediaDisposable: Disposable? - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group?, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group?, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.interaction = interaction self.key = key self.peersFilter = peersFilter @@ -1691,7 +1697,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } - let chatListInteraction = ChatListNodeInteraction(context: context, activateSearch: { + let chatListInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { }, peerSelected: { [weak self] peer, chatPeer, _ in interaction.dismissInput() interaction.openPeer(peer, chatPeer, false) @@ -2504,7 +2510,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let insets = UIEdgeInsets(top: topPanelHeight, left: sideInset, bottom: bottomInset, right: sideInset) self.shimmerNode.frame = CGRect(origin: CGPoint(x: overflowInset, y: topInset), size: CGSize(width: size.width - overflowInset * 2.0, height: size.height)) - self.shimmerNode.update(context: self.context, size: CGSize(width: size.width - overflowInset * 2.0, height: size.height), presentationData: self.presentationData, key: !(self.searchQueryValue?.isEmpty ?? true) && self.key == .media ? .chats : self.key, hasSelection: self.selectedMessages != nil, transition: transition) + self.shimmerNode.update(context: self.context, size: CGSize(width: size.width - overflowInset * 2.0, height: size.height), presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, key: !(self.searchQueryValue?.isEmpty ?? true) && self.key == .media ? .chats : self.key, hasSelection: self.selectedMessages != nil, transition: transition) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.recentListNode.frame = CGRect(origin: CGPoint(), size: size) @@ -2914,7 +2920,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { self.addSubnode(self.maskNode) } - func update(context: AccountContext, size: CGSize, presentationData: PresentationData, key: ChatListSearchPaneKey, hasSelection: Bool, transition: ContainedViewLayoutTransition) { + func update(context: AccountContext, size: CGSize, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, key: ChatListSearchPaneKey, hasSelection: Bool, transition: ContainedViewLayoutTransition) { if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData || self.currentParams?.key != key { self.currentParams = (size, presentationData, key) @@ -2924,7 +2930,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { let timestamp1: Int32 = 100000 var peers: [EnginePeer.Id: EnginePeer] = [:] peers[peer1.id] = peer1 - let interaction = ChatListNodeInteraction(context: context, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() }, present: { _ in }) diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index a201c7d6e2..555176ab9f 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -7,6 +7,8 @@ import TelegramPresentationData import TelegramCore import AccountContext import ContextUI +import AnimationCache +import MultiAnimationRenderer protocol ChatListSearchPaneNode: ASDisplayNode { var isReady: Signal { get } @@ -102,6 +104,8 @@ private final class ChatListSearchPendingPane { init( context: AccountContext, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)?, interaction: ChatListSearchInteraction, navigationController: NavigationController?, @@ -112,7 +116,7 @@ private final class ChatListSearchPendingPane { key: ChatListSearchPaneKey, hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void ) { - let paneNode = ChatListSearchListPaneNode(context: context, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: key == .chats ? peersFilter : [], groupId: groupId, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) + let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: key == .chats ? peersFilter : [], groupId: groupId, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode) self.disposable = (paneNode.isReady @@ -130,6 +134,8 @@ private final class ChatListSearchPendingPane { final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private let context: AccountContext + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let updatedPresentationData: (initial: PresentationData, signal: Signal)? private let peersFilter: ChatListNodePeersFilter private let groupId: EngineChatList.Group @@ -166,8 +172,10 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD private var currentAvailablePanes: [ChatListSearchPaneKey]? - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.updatedPresentationData = updatedPresentationData self.peersFilter = peersFilter self.groupId = groupId @@ -394,6 +402,8 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD var leftScope = false let pane = ChatListSearchPendingPane( context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, updatedPresentationData: self.updatedPresentationData, interaction: self.interaction!, navigationController: self.navigationController, diff --git a/submodules/ChatListUI/Sources/ChatListTitleView.swift b/submodules/ChatListUI/Sources/ChatListTitleView.swift index 6ab72bd444..c6f9eb02f3 100644 --- a/submodules/ChatListUI/Sources/ChatListTitleView.swift +++ b/submodules/ChatListUI/Sources/ChatListTitleView.swift @@ -4,19 +4,33 @@ import AsyncDisplayKit import Display import TelegramPresentationData import ActivityIndicator +import ComponentFlow +import EmojiStatusComponent +import AnimationCache +import MultiAnimationRenderer +import TelegramCore +import ComponentDisplayAdapters +import AccountContext private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) struct NetworkStatusTitle: Equatable { + enum Status: Equatable { + case premium + case emoji(PeerEmojiStatus) + } + let text: String let activity: Bool let hasProxy: Bool let connectsViaProxy: Bool let isPasscodeSet: Bool let isManuallyLocked: Bool + let peerStatus: Status? } final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitleTransitionNode { + private let context: AccountContext private let titleNode: ImmediateTextNode private let lockView: ChatListTitleLockView private weak var lockSnapshotView: UIView? @@ -24,10 +38,15 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl private let buttonView: HighlightTrackingButton private let proxyNode: ChatTitleProxyNode private let proxyButton: HighlightTrackingButton + private var titleCredibilityIconView: ComponentHostView? + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + + var openStatusSetup: ((UIView) -> Void)? private var validLayout: (CGSize, CGRect)? - private var _title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false) + private var _title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: nil) var title: NetworkStatusTitle { get { return self._title @@ -91,6 +110,66 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl } self.lockView.updateTheme(self.theme) + let animateStatusTransition = !oldValue.text.isEmpty && oldValue.peerStatus != title.peerStatus + + if let peerStatus = title.peerStatus { + let statusContent: EmojiStatusComponent.Content + switch peerStatus { + case .premium: + statusContent = .premium(color: self.theme.list.itemAccentColor) + case let .emoji(emoji): + statusContent = .emojiStatus(status: emoji, size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor) + } + + var titleCredibilityIconTransition: Transition + if animateStatusTransition { + titleCredibilityIconTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + } else { + titleCredibilityIconTransition = .immediate + } + let titleCredibilityIconView: ComponentHostView + if let current = self.titleCredibilityIconView { + titleCredibilityIconView = current + } else { + titleCredibilityIconTransition = .immediate + titleCredibilityIconView = ComponentHostView() + self.titleCredibilityIconView = titleCredibilityIconView + self.addSubview(titleCredibilityIconView) + } + + let _ = titleCredibilityIconView.update( + transition: titleCredibilityIconTransition, + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + content: statusContent, + action: { [weak self] in + guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else { + return + } + strongSelf.openStatusSetup?(titleCredibilityIconView) + }, + longTapAction: nil + )), + environment: {}, + containerSize: CGSize(width: 22.0, height: 22.0) + ) + } else { + if let titleCredibilityIconView = self.titleCredibilityIconView { + self.titleCredibilityIconView = nil + + if animateStatusTransition { + titleCredibilityIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak titleCredibilityIconView] _ in + titleCredibilityIconView?.removeFromSuperview() + }) + titleCredibilityIconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + titleCredibilityIconView.removeFromSuperview() + } + } + } + self.setNeedsLayout() } } @@ -118,10 +197,14 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl } } - init(theme: PresentationTheme, strings: PresentationStrings) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) { + self.context = context self.theme = theme self.strings = strings + self.animationCache = animationCache + self.animationRenderer = animationRenderer + self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 1 @@ -253,7 +336,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl let buttonX = max(0.0, titleFrame.minX - 10.0) self.buttonView.frame = CGRect(origin: CGPoint(x: buttonX, y: 0.0), size: CGSize(width: min(titleFrame.maxX + 28.0, size.width) - buttonX, height: size.height)) - let lockFrame = CGRect(x: titleFrame.maxX + 6.0, y: titleFrame.minY + 2.0, width: 2.0, height: 2.0) + let lockFrame = CGRect(x: titleFrame.minX - 6.0 - 12.0, y: titleFrame.minY + 2.0, width: 2.0, height: 2.0) transition.updateFrame(view: self.lockView, frame: lockFrame) if let lockSnapshotView = self.lockSnapshotView { transition.updateFrame(view: lockSnapshotView, frame: lockFrame) @@ -261,6 +344,60 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl let activityIndicatorFrame = CGRect(origin: CGPoint(x: titleFrame.minX - indicatorSize.width - 4.0, y: titleFrame.minY - 1.0), size: indicatorSize) transition.updateFrame(node: self.activityIndicator, frame: activityIndicatorFrame) + + if let peerStatus = self.title.peerStatus { + let statusContent: EmojiStatusComponent.Content + switch peerStatus { + case .premium: + statusContent = .premium(color: self.theme.list.itemAccentColor) + case let .emoji(emoji): + statusContent = .emojiStatus(status: emoji, size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor) + } + + var titleCredibilityIconTransition = Transition(transition) + let titleCredibilityIconView: ComponentHostView + if let current = self.titleCredibilityIconView { + titleCredibilityIconView = current + } else { + titleCredibilityIconTransition = .immediate + titleCredibilityIconView = ComponentHostView() + self.titleCredibilityIconView = titleCredibilityIconView + self.addSubview(titleCredibilityIconView) + } + + let titleIconSize = titleCredibilityIconView.update( + transition: titleCredibilityIconTransition, + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + content: statusContent, + action: { [weak self] in + guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else { + return + } + strongSelf.openStatusSetup?(titleCredibilityIconView) + }, + longTapAction: nil + )), + environment: {}, + containerSize: CGSize(width: 22.0, height: 22.0) + ) + titleCredibilityIconTransition.setFrame(view: titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: floorToScreenPixels(titleFrame.midY - titleIconSize.height / 2.0)), size: titleIconSize)) + } else { + if let titleCredibilityIconView = self.titleCredibilityIconView { + self.titleCredibilityIconView = nil + + if transition.isAnimated { + titleCredibilityIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak titleCredibilityIconView] _ in + titleCredibilityIconView?.removeFromSuperview() + }) + titleCredibilityIconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + titleCredibilityIconView.removeFromSuperview() + } + } + } } @objc private func buttonPressed() { @@ -272,11 +409,10 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl } func makeTransitionMirrorNode() -> ASDisplayNode { - let view = ChatListTitleView(theme: self.theme, strings: self.strings) - view.title = self.title + let snapshotView = self.snapshotView(afterScreenUpdates: false) return ASDisplayNode(viewBlock: { - return view + return snapshotView ?? UIView() }, didLoad: nil) } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 6630b2a315..cb0635854b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -81,7 +81,30 @@ public final class ChatListNodeInteraction { let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer - public init(context: AccountContext, activateSearch: @escaping () -> Void, peerSelected: @escaping (EnginePeer, EnginePeer?, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (EnginePeer) -> Void, togglePeerSelected: @escaping (EnginePeer) -> Void, togglePeersSelection: @escaping ([PeerEntry], Bool) -> Void, additionalCategorySelected: @escaping (Int) -> Void, messageSelected: @escaping (EnginePeer, EngineMessage, ChatListNodeEntryPromoInfo?) -> Void, groupSelected: @escaping (EngineChatList.Group) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, setItemPinned: @escaping (EngineChatList.PinnedItem.Id, Bool) -> Void, setPeerMuted: @escaping (EnginePeer.Id, Bool) -> Void, deletePeer: @escaping (EnginePeer.Id, Bool) -> Void, updatePeerGrouping: @escaping (EnginePeer.Id, Bool) -> Void, togglePeerMarkedUnread: @escaping (EnginePeer.Id, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, hidePsa: @escaping (EnginePeer.Id) -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, present: @escaping (ViewController) -> Void) { + public init( + context: AccountContext, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + activateSearch: @escaping () -> Void, + peerSelected: @escaping (EnginePeer, EnginePeer?, ChatListNodeEntryPromoInfo?) -> Void, + disabledPeerSelected: @escaping (EnginePeer) -> Void, + togglePeerSelected: @escaping (EnginePeer) -> Void, + togglePeersSelection: @escaping ([PeerEntry], Bool) -> Void, + additionalCategorySelected: @escaping (Int) -> Void, + messageSelected: @escaping (EnginePeer, EngineMessage, ChatListNodeEntryPromoInfo?) -> Void, + groupSelected: @escaping (EngineChatList.Group) -> Void, + addContact: @escaping (String) -> Void, + setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, + setItemPinned: @escaping (EngineChatList.PinnedItem.Id, Bool) -> Void, + setPeerMuted: @escaping (EnginePeer.Id, Bool) -> Void, + deletePeer: @escaping (EnginePeer.Id, Bool) -> Void, + updatePeerGrouping: @escaping (EnginePeer.Id, Bool) -> Void, + togglePeerMarkedUnread: @escaping (EnginePeer.Id, Bool) -> Void, + toggleArchivedFolderHiddenByDefault: @escaping () -> Void, + hidePsa: @escaping (EnginePeer.Id) -> Void, + activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, + present: @escaping (ViewController) -> Void + ) { self.activateSearch = activateSearch self.peerSelected = peerSelected self.disabledPeerSelected = disabledPeerSelected @@ -101,11 +124,8 @@ public final class ChatListNodeInteraction { self.hidePsa = hidePsa self.activateChatPreview = activateChatPreview self.present = present - - self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { - return TempBox.shared.tempFile(fileName: "file").path - }) - self.animationRenderer = MultiAnimationRendererImpl() + self.animationCache = animationCache + self.animationRenderer = animationRenderer } } @@ -605,6 +625,8 @@ public final class ChatListNode: ListView { private let context: AccountContext private let groupId: EngineChatList.Group private let mode: ChatListNodeMode + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let _ready = ValuePromise() private var didSetReady = false @@ -719,13 +741,15 @@ public final class ChatListNode: ListView { public var selectionLimit: Int32 = 100 public var reachedSelectionLimit: ((Int32) -> Void)? - public init(context: AccountContext, groupId: EngineChatList.Group, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) { + public init(context: AccountContext, groupId: EngineChatList.Group, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool) { self.context = context self.groupId = groupId self.chatListFilter = chatListFilter self.chatListFilterValue.set(.single(chatListFilter)) self.fillPreloadItems = fillPreloadItems self.mode = mode + self.animationCache = animationCache + self.animationRenderer = animationRenderer var isSelecting = false if case .peers(_, true, _, _) = mode { @@ -744,7 +768,7 @@ public final class ChatListNode: ListView { self.keepMinimalScrollHeightWithTopInset = navigationBarSearchContentHeight - let nodeInteraction = ChatListNodeInteraction(context: context, activateSearch: { [weak self] in + let nodeInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { [weak self] in if let strongSelf = self, let activateSearch = strongSelf.activateSearch { activateSearch() } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index c4095fbfba..dceb3994b3 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -351,7 +351,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState readState: nil, isRemovedFromTotalUnreadCount: false, draftState: nil, - peer: EngineRenderedPeer(peerId: peer.0.id, peers: peers), + peer: EngineRenderedPeer(peerId: peer.0.id, peers: peers, associatedMedia: [:]), presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, @@ -369,7 +369,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState } } - result.append(.PeerEntry(index: EngineChatList.Item.Index.absoluteUpperBound.predecessor, presentationData: state.presentationData, messages: [], readState: nil, isRemovedFromTotalUnreadCount: false, draftState: nil, peer: EngineRenderedPeer(peerId: savedMessagesPeer.id, peers: [savedMessagesPeer.id: savedMessagesPeer]), presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, editing: state.editing, hasActiveRevealControls: false, selected: state.selectedPeerIds.contains(savedMessagesPeer.id), inputActivities: nil, promoInfo: nil, hasFailedMessages: false, isContact: false)) + result.append(.PeerEntry(index: EngineChatList.Item.Index.absoluteUpperBound.predecessor, presentationData: state.presentationData, messages: [], readState: nil, isRemovedFromTotalUnreadCount: false, draftState: nil, peer: EngineRenderedPeer(peerId: savedMessagesPeer.id, peers: [savedMessagesPeer.id: savedMessagesPeer], associatedMedia: [:]), presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, editing: state.editing, hasActiveRevealControls: false, selected: state.selectedPeerIds.contains(savedMessagesPeer.id), inputActivities: nil, promoInfo: nil, hasFailedMessages: false, isContact: false)) } else { if !filteredAdditionalItemEntries.isEmpty { for item in filteredAdditionalItemEntries.reversed() { diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index cd85bff8bc..efcacf3237 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1521,7 +1521,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } if !items.reactionItems.isEmpty, let context = items.context, let animationCache = items.animationCache { - let reactionContextNode = ReactionContextNode(context: context, animationCache: animationCache, presentationData: self.presentationData, items: items.reactionItems, getEmojiContent: items.getEmojiContent, isExpandedUpdated: { _ in }) + let reactionContextNode = ReactionContextNode(context: context, animationCache: animationCache, presentationData: self.presentationData, items: items.reactionItems, getEmojiContent: items.getEmojiContent, isExpandedUpdated: { _ in }, requestLayout: { _ in }) self.reactionContextNode = reactionContextNode self.addSubnode(reactionContextNode) diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 4e56f650c0..650e58c4a3 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -324,6 +324,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo func scrollViewDidScroll(_ scrollView: UIScrollView) { var adjustedBounds = scrollView.bounds var topOverscroll: CGFloat = 0.0 + switch self.overscrollMode { case .unrestricted: if adjustedBounds.origin.y < 0.0 { @@ -336,10 +337,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if adjustedBounds.origin.y > 0.0 { adjustedBounds.origin.y = 0.0 } else { + adjustedBounds.origin.y = floorToScreenPixels(adjustedBounds.origin.y * 0.35) topOverscroll = -adjustedBounds.origin.y } } else { if adjustedBounds.origin.y < 0.0 { + adjustedBounds.origin.y = floorToScreenPixels(adjustedBounds.origin.y * 0.35) topOverscroll = -adjustedBounds.origin.y } else if adjustedBounds.origin.y + adjustedBounds.height > scrollView.contentSize.height { adjustedBounds.origin.y = scrollView.contentSize.height - adjustedBounds.height @@ -353,7 +356,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo reactionContextNode.updateIsIntersectingContent(isIntersectingContent: isIntersectingContent, transition: .animated(duration: 0.25, curve: .easeInOut)) if !reactionContextNode.isExpanded && reactionContextNode.canBeExpanded { - if topOverscroll > 60.0 && self.scroller.isDragging { + if topOverscroll > 30.0 && self.scroller.isDragging { self.scroller.panGestureRecognizer.state = .cancelled reactionContextNode.expand() } else { @@ -516,6 +519,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } strongSelf.setCurrentReactionsPositionLock() strongSelf.requestUpdate(transition) + }, + requestLayout: { [weak self] transition in + guard let strongSelf = self else { + return + } + strongSelf.requestUpdate(transition) } ) self.reactionContextNode = reactionContextNode @@ -538,7 +547,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo controller.premiumReactionsSelected?() } } - contentTopInset += reactionContextNode.currentContentHeight + 18.0 + contentTopInset += reactionContextNode.contentHeight + 18.0 } else if let reactionContextNode = self.reactionContextNode { self.reactionContextNode = nil removedReactionContextNode = reactionContextNode @@ -636,7 +645,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo isAnimatingOut = true } else { if let currentReactionsPositionLock = self.currentReactionsPositionLock, let reactionContextNode = self.reactionContextNode { - contentRect.origin.y = currentReactionsPositionLock + reactionContextNode.currentContentHeight + 18.0 + contentRect.origin.y = currentReactionsPositionLock + reactionContextNode.contentHeight + 18.0 + reactionContextNode.visibleExtensionDistance } else if let topPositionLock = self.actionsStackNode.topPositionLock { contentRect.origin.y = topPositionLock - contentActionsSpacing - contentRect.height } else if keepInPlace { @@ -664,7 +673,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo reactionContextNodeTransition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), anchorRect: contentRect.offsetBy(dx: contentParentGlobalFrame.minX, dy: 0.0), isAnimatingOut: isAnimatingOut, transition: reactionContextNodeTransition) - self.proposedReactionsPositionLock = contentRect.minY - 18.0 - reactionContextNode.currentContentHeight - 46.0 + self.proposedReactionsPositionLock = contentRect.minY - 18.0 - reactionContextNode.contentHeight - 46.0 } else { self.proposedReactionsPositionLock = nil } @@ -697,6 +706,10 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo contentVerticalOffset = delta } } + var additionalVisibleOffsetY: CGFloat = 0.0 + if let reactionContextNode = self.reactionContextNode { + additionalVisibleOffsetY += reactionContextNode.visibleExtensionDistance + } if centerActionsHorizontally { actionsFrame.origin.x = floor(contentParentGlobalFrame.minX + contentRect.midX - actionsFrame.width / 2.0) if actionsFrame.maxX > layout.size.width - actionsEdgeInset { @@ -731,10 +744,10 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo actionsFrame.origin.x = actionsEdgeInset } } - transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame, beginWithCurrentState: true) + transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame.offsetBy(dx: 0.0, dy: additionalVisibleOffsetY), beginWithCurrentState: true) if let contentNode = contentNode { - contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX, y: contentRect.minY - contentNode.containingItem.contentRect.minY + contentVerticalOffset), size: contentNode.containingItem.view.bounds.size), beginWithCurrentState: true) + contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX, y: contentRect.minY - contentNode.containingItem.contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentNode.containingItem.view.bounds.size), beginWithCurrentState: true) } let contentHeight: CGFloat diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 3f4e52b5d7..b378d6b817 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -263,6 +263,24 @@ public extension ContainedViewLayoutTransition { } } + func updateFrameAdditive(view: UIView, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if view.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + view.frame = frame + if let completion = completion { + completion(true) + } + case .animated: + let previousFrame = view.frame + view.frame = frame + self.animatePositionAdditive(layer: view.layer, offset: CGPoint(x: previousFrame.minX - frame.minX, y: previousFrame.minY - frame.minY)) + } + } + } + func updateFrameAdditiveToCenter(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.frame.equalTo(frame) && !force { completion?(true) @@ -283,6 +301,26 @@ public extension ContainedViewLayoutTransition { } } + func updateFrameAdditiveToCenter(view: UIView, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if view.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + view.center = frame.center + view.bounds = CGRect(origin: view.bounds.origin, size: frame.size) + if let completion = completion { + completion(true) + } + case .animated: + let previousCenter = view.frame.center + view.center = frame.center + view.bounds = CGRect(origin: view.bounds.origin, size: frame.size) + self.animatePositionAdditive(layer: view.layer, offset: CGPoint(x: previousCenter.x - frame.midX, y: previousCenter.y - frame.midY)) + } + } + } + func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.bounds.equalTo(bounds) && !force { completion?(true) diff --git a/submodules/HashtagSearchUI/BUILD b/submodules/HashtagSearchUI/BUILD index 3f24ed0255..91c74fef79 100644 --- a/submodules/HashtagSearchUI/BUILD +++ b/submodules/HashtagSearchUI/BUILD @@ -21,6 +21,9 @@ swift_library( "//submodules/SegmentedControlNode:SegmentedControlNode", "//submodules/ListMessageItem:ListMessageItem", "//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader", + "//submodules/Postbox:Postbox", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", ], visibility = [ "//visibility:public", diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 79582574cd..f1a52b3e0b 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -2,12 +2,15 @@ import Foundation import UIKit import Display import TelegramCore +import Postbox import SwiftSignalKit import TelegramPresentationData import TelegramBaseController import AccountContext import ChatListUI import ListMessageItem +import AnimationCache +import MultiAnimationRenderer public final class HashtagSearchController: TelegramBaseController { private let queue = Queue() @@ -21,6 +24,9 @@ public final class HashtagSearchController: TelegramBaseController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + private var controllerNode: HashtagSearchControllerNode { return self.displayNode as! HashtagSearchControllerNode } @@ -30,6 +36,11 @@ public final class HashtagSearchController: TelegramBaseController { self.peer = peer self.query = query + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none, groupCallPanelSource: .none) @@ -47,7 +58,7 @@ public final class HashtagSearchController: TelegramBaseController { let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap(EnginePeerReadCounters.init), chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic, false) }) } - let interaction = ChatListNodeInteraction(context: context, activateSearch: { + let interaction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { }, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in diff --git a/submodules/Postbox/Sources/ChatListView.swift b/submodules/Postbox/Sources/ChatListView.swift index 31bfbf3416..e8c11793fe 100644 --- a/submodules/Postbox/Sources/ChatListView.swift +++ b/submodules/Postbox/Sources/ChatListView.swift @@ -335,6 +335,60 @@ public struct ChatListAdditionalItemEntry: Equatable { } } +func renderAssociatedMediaForPeers(postbox: PostboxImpl, peers: SimpleDictionary) -> [MediaId: Media] { + var result: [MediaId: Media] = [:] + + for (_, peer) in peers { + if let associatedMediaIds = peer.associatedMediaIds { + for id in associatedMediaIds { + if result[id] == nil { + if let media = postbox.messageHistoryTable.getMedia(id) { + result[id] = media + } + } + } + } + } + + return result +} + +func renderAssociatedMediaForPeers(postbox: PostboxImpl, peers: [Peer]) -> [MediaId: Media] { + var result: [MediaId: Media] = [:] + + for peer in peers { + if let associatedMediaIds = peer.associatedMediaIds { + for id in associatedMediaIds { + if result[id] == nil { + if let media = postbox.messageHistoryTable.getMedia(id) { + result[id] = media + } + } + } + } + } + + return result +} + +func renderAssociatedMediaForPeers(postbox: PostboxImpl, peers: [PeerId: Peer]) -> [MediaId: Media] { + var result: [MediaId: Media] = [:] + + for (_, peer) in peers { + if let associatedMediaIds = peer.associatedMediaIds { + for id in associatedMediaIds { + if result[id] == nil { + if let media = postbox.messageHistoryTable.getMedia(id) { + result[id] = media + } + } + } + } + } + + return result +} + final class MutableChatListView { let groupId: PeerGroupId let filterPredicate: ChatListFilterPredicate? @@ -448,7 +502,7 @@ final class MutableChatListView { } } - let renderedPeer = RenderedPeer(peerId: peer.id, peers: peers) + let renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: postbox, peers: peers)) let isUnread = postbox.readStateTable.getCombinedState(peer.id)?.isUnread ?? false renderedPeers.append(ChatListGroupReferencePeer(peer: renderedPeer, isUnread: isUnread)) @@ -601,7 +655,7 @@ final class MutableChatListView { } } - return .MessageEntry(index: index, messages: renderedMessages, readState: postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId), notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: false, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId), renderedPeer: RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers), presence: presence, tagSummaryInfo: [:], hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact: isContact) + return .MessageEntry(index: index, messages: renderedMessages, readState: postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId), notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: false, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId), renderedPeer: RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: postbox, peers: peers)), presence: presence, tagSummaryInfo: [:], hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact: isContact) default: return nil } diff --git a/submodules/Postbox/Sources/ChatListViewState.swift b/submodules/Postbox/Sources/ChatListViewState.swift index baf350c840..c1aeaeb688 100644 --- a/submodules/Postbox/Sources/ChatListViewState.swift +++ b/submodules/Postbox/Sources/ChatListViewState.swift @@ -101,7 +101,7 @@ private func updateMessagePeers(_ message: Message, updatedPeers: [PeerId: Peer] return nil } -private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [PeerId: Peer]) -> RenderedPeer? { +private func updatedRenderedPeer(postbox: PostboxImpl, renderedPeer: RenderedPeer, updatedPeers: [PeerId: Peer]) -> RenderedPeer? { var updated = false for (peerId, currentPeer) in renderedPeer.peers { if let updatedPeer = updatedPeers[peerId], !arePeersEqual(currentPeer, updatedPeer) { @@ -118,7 +118,7 @@ private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [Pe peers[peerId] = currentPeer } } - return RenderedPeer(peerId: renderedPeer.peerId, peers: peers) + return RenderedPeer(peerId: renderedPeer.peerId, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: postbox, peers: peers)) } return nil } @@ -666,7 +666,7 @@ private final class ChatListViewSpaceState { hasUpdatedMessages = true } } - let renderedPeer = updatedRenderedPeer(entryRenderedPeer, updatedPeers: transaction.currentUpdatedPeers) + let renderedPeer = updatedRenderedPeer(postbox: postbox, renderedPeer: entryRenderedPeer, updatedPeers: transaction.currentUpdatedPeers) if hasUpdatedMessages || renderedPeer != nil { return .MessageEntry( @@ -1416,7 +1416,7 @@ struct ChatListViewState { presence = postbox.peerPresenceTable.get(index.messageIndex.id.peerId) } } - let renderedPeer = RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers) + let renderedPeer = RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: postbox, peers: peers)) var tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo] = [:] for (key, component) in self.summaryComponents.components { diff --git a/submodules/Postbox/Sources/Media.swift b/submodules/Postbox/Sources/Media.swift index 68e7a23038..70d6f75241 100644 --- a/submodules/Postbox/Sources/Media.swift +++ b/submodules/Postbox/Sources/Media.swift @@ -82,6 +82,34 @@ public protocol Media: AnyObject, PostboxCoding { func isSemanticallyEqual(to other: Media) -> Bool } +public func areMediaArraysEqual(_ lhs: [Media], _ rhs: [Media]) -> Bool { + if lhs.count != rhs.count { + return false + } + for i in 0 ..< lhs.count { + if !lhs[i].isEqual(to: rhs[i]) { + return false + } + } + return true +} + +public func areMediaDictionariesEqual(_ lhs: [MediaId: Media], _ rhs: [MediaId: Media]) -> Bool { + if lhs.count != rhs.count { + return false + } + for (key, value) in lhs { + if let rhsValue = rhs[key] { + if !value.isEqual(to: rhsValue) { + return false + } + } else { + return false + } + } + return true +} + public extension Media { func isLikelyToBeUpdated() -> Bool { return false diff --git a/submodules/Postbox/Sources/Peer.swift b/submodules/Postbox/Sources/Peer.swift index d760dece99..a3675a7fcd 100644 --- a/submodules/Postbox/Sources/Peer.swift +++ b/submodules/Postbox/Sources/Peer.swift @@ -299,6 +299,7 @@ public protocol Peer: AnyObject, PostboxCoding { var indexName: PeerIndexNameRepresentation { get } var associatedPeerId: PeerId? { get } var notificationSettingsPeerId: PeerId? { get } + var associatedMediaIds: [MediaId]? { get } func isEqual(_ other: Peer) -> Bool } diff --git a/submodules/Postbox/Sources/PeerView.swift b/submodules/Postbox/Sources/PeerView.swift index 6db9f56c81..b9a3639216 100644 --- a/submodules/Postbox/Sources/PeerView.swift +++ b/submodules/Postbox/Sources/PeerView.swift @@ -24,6 +24,7 @@ final class MutablePeerView: MutablePostboxView { var peers: [PeerId: Peer] = [:] var peerPresences: [PeerId: PeerPresence] = [:] var messages: [MessageId: Message] = [:] + var media: [MediaId: Media] = [:] var peerIsContact: Bool var groupId: PeerGroupId? @@ -81,6 +82,7 @@ final class MutablePeerView: MutablePostboxView { self.messages[id] = message } } + self.media = renderAssociatedMediaForPeers(postbox: postbox, peers: self.peers) } func reset(postbox: PostboxImpl) -> Bool { @@ -103,7 +105,7 @@ final class MutablePeerView: MutablePostboxView { } var updated = false - + var peersUpdated = false var updateMessages = false if let cachedData = updatedCachedPeerData[self.contactPeerId], self.cachedData == nil || !self.cachedData!.isEqual(to: cachedData) { @@ -124,8 +126,10 @@ final class MutablePeerView: MutablePostboxView { for id in peerIds { if let peer = updatedPeers[id] { self.peers[id] = peer + peersUpdated = true } else if let peer = getPeer(id) { self.peers[id] = peer + peersUpdated = true } if let presence = updatedPeerPresences[id] { @@ -170,6 +174,7 @@ final class MutablePeerView: MutablePostboxView { if let peer = updatedPeers[id] { self.peers[id] = peer updated = true + peersUpdated = true } if let presence = updatedPeerPresences[id] { self.peerPresences[id] = presence @@ -178,6 +183,10 @@ final class MutablePeerView: MutablePostboxView { } } + if peersUpdated { + self.media = renderAssociatedMediaForPeers(postbox: postbox, peers: self.peers) + } + if let cachedData = self.cachedData, !cachedData.messageIds.isEmpty, let operations = transaction.currentOperationsByPeerId[self.peerId] { outer: for operation in operations { switch operation { @@ -270,6 +279,7 @@ public final class PeerView: PostboxView { public let peers: [PeerId: Peer] public let peerPresences: [PeerId: PeerPresence] public let messages: [MessageId: Message] + public let media: [MediaId: Media] public let peerIsContact: Bool public let groupId: PeerGroupId? @@ -280,6 +290,7 @@ public final class PeerView: PostboxView { self.peers = mutableView.peers self.peerPresences = mutableView.peerPresences self.messages = mutableView.messages + self.media = mutableView.media self.peerIsContact = mutableView.peerIsContact self.groupId = mutableView.groupId } diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 1af2ada131..759dac9868 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -3040,7 +3040,7 @@ final class PostboxImpl { peers[associatedPeer.id] = associatedPeer } } - chatPeers.append(RenderedPeer(peerId: peer.id, peers: peers)) + chatPeers.append(RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: self, peers: peers))) peerIds.insert(peerId) } } @@ -3051,7 +3051,7 @@ final class PostboxImpl { if let peer = self.peerTable.get(peerId) { var peers = SimpleDictionary() peers[peer.id] = peer - contactPeers.append(RenderedPeer(peerId: peer.id, peers: peers)) + contactPeers.append(RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: self, peers: peers))) } } } diff --git a/submodules/Postbox/Sources/RenderedPeer.swift b/submodules/Postbox/Sources/RenderedPeer.swift index c80a30ee1e..b9ab8c253d 100644 --- a/submodules/Postbox/Sources/RenderedPeer.swift +++ b/submodules/Postbox/Sources/RenderedPeer.swift @@ -3,15 +3,18 @@ import Foundation public final class RenderedPeer: Equatable { public let peerId: PeerId public let peers: SimpleDictionary + public let associatedMedia: [MediaId: Media] - public init(peerId: PeerId, peers: SimpleDictionary) { + public init(peerId: PeerId, peers: SimpleDictionary, associatedMedia: [MediaId: Media]) { self.peerId = peerId self.peers = peers + self.associatedMedia = associatedMedia } public init(peer: Peer) { self.peerId = peer.id self.peers = SimpleDictionary([peer.id: peer]) + self.associatedMedia = [:] } public static func ==(lhs: RenderedPeer, rhs: RenderedPeer) -> Bool { @@ -26,6 +29,9 @@ public final class RenderedPeer: Equatable { }) { return false } + if !areMediaDictionariesEqual(lhs.associatedMedia, rhs.associatedMedia) { + return false + } return true } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 1b0d0aa5ad..900e2cd679 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -18,6 +18,8 @@ import EntityKeyboard import ComponentDisplayAdapters import AnimationCache import MultiAnimationRenderer +import EmojiTextAttachmentView +import TextFormat public final class ReactionItem { public struct Reaction: Equatable { @@ -108,8 +110,8 @@ private final class ExpandItemView: UIView { } func update(size: CGSize, transition: ContainedViewLayoutTransition) { - self.layer.cornerRadius = size.width / 2.0 - self.tintView.layer.cornerRadius = size.width / 2.0 + transition.updateCornerRadius(layer: self.layer, cornerRadius: size.width / 2.0) + transition.updateCornerRadius(layer: self.tintView.layer, cornerRadius: size.width / 2.0) if let image = self.arrowView.image { transition.updateFrame(view: self.arrowView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels(size.height - size.width + (size.width - image.size.height) / 2.0)), size: image.size)) @@ -125,6 +127,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private let items: [ReactionContextItem] private let getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? private let isExpandedUpdated: (ContainedViewLayoutTransition) -> Void + private let requestLayout: (ContainedViewLayoutTransition) -> Void private let backgroundNode: ReactionContextBackgroundNode @@ -137,6 +140,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private let scrollNode: ASScrollNode private let previewingItemContainer: ASDisplayNode private var visibleItemNodes: [Int: ReactionItemNode] = [:] + private var disappearingVisibleItemNodes: [Int: ReactionItemNode] = [:] private var visibleItemMaskNodes: [Int: ASDisplayNode] = [:] private let expandItemView: ExpandItemView? @@ -166,12 +170,17 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var didAnimateIn: Bool = false - public private(set) var currentContentHeight: CGFloat = 46.0 + public var contentHeight: CGFloat { + return self.currentContentHeight + } + + private var currentContentHeight: CGFloat = 46.0 public private(set) var isExpanded: Bool = false public private(set) var canBeExpanded: Bool = false private var animateFromExtensionDistance: CGFloat = 0.0 private var extensionDistance: CGFloat = 0.0 + public private(set) var visibleExtensionDistance: CGFloat = 0.0 private var emojiContentLayout: EmojiPagerContentComponent.CustomLayout? private var emojiContent: EmojiPagerContentComponent? @@ -181,12 +190,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var horizontalExpandStartLocation: CGPoint? private var horizontalExpandDistance: CGFloat = 0.0 - public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void) { + public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) { self.context = context self.presentationData = presentationData self.items = items self.getEmojiContent = getEmojiContent self.isExpandedUpdated = isExpandedUpdated + self.requestLayout = requestLayout self.animationCache = animationCache self.animationRenderer = MultiAnimationRendererImpl() @@ -278,7 +288,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if self.canBeExpanded { let horizontalExpandRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.horizontalExpandGesture(_:))) - //self.view.addGestureRecognizer(horizontalExpandRecognizer) + self.view.addGestureRecognizer(horizontalExpandRecognizer) self.horizontalExpandRecognizer = horizontalExpandRecognizer } } @@ -306,19 +316,28 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if let horizontalExpandStartLocation = self.horizontalExpandStartLocation { let currentLocation = recognizer.location(in: self.view) - let distance = min(0.0, currentLocation.x - horizontalExpandStartLocation.x) + let distance = -min(0.0, currentLocation.x - horizontalExpandStartLocation.x) self.horizontalExpandDistance = distance - if let (size, insets, anchorRect) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) - } + let maxCompressionDistance: CGFloat = 100.0 + var compressionFactor: CGFloat = max(0.0, min(1.0, self.horizontalExpandDistance / maxCompressionDistance)) + compressionFactor = compressionFactor * compressionFactor + + self.extensionDistance = 20.0 * compressionFactor + self.visibleExtensionDistance = self.extensionDistance + + self.requestLayout(.immediate) } case .cancelled, .ended: if self.horizontalExpandDistance != 0.0 { - self.horizontalExpandDistance = 0.0 - - if let (size, insets, anchorRect) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isAnimatingOut: false, transition: .animated(duration: 0.3, curve: .spring), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) + if self.horizontalExpandDistance >= 90.0 { + self.expand() + } else { + self.horizontalExpandDistance = 0.0 + self.extensionDistance = 0.0 + self.visibleExtensionDistance = 0.0 + + self.requestLayout(.animated(duration: 0.4, curve: .spring)) } } default: @@ -430,16 +449,24 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { var currentMaskFrame: CGRect? var maskTransition: ContainedViewLayoutTransition? - let topVisibleItems: Int + let maxCompressionDistance: CGFloat = 100.0 + let compressionFactor: CGFloat = max(0.0, min(1.0, self.horizontalExpandDistance / maxCompressionDistance)) + let minItemSpacing: CGFloat = 2.0 + let effectiveItemSpacing: CGFloat = minItemSpacing + (1.0 - compressionFactor) * (itemSpacing - minItemSpacing) + + var topVisibleItems: Int if self.getEmojiContent != nil { topVisibleItems = min(self.items.count, visibleItemCount - 1) + if compressionFactor >= 0.6 { + topVisibleItems = min(self.items.count, visibleItemCount) + } } else { topVisibleItems = self.items.count } var validIndices = Set() var nextX: CGFloat = sideInset - for i in 0 ..< topVisibleItems { + for i in 0 ..< self.items.count { var currentItemSize = itemSize if let highlightedReactionIndex = highlightedReactionIndex { if highlightedReactionIndex == i { @@ -456,13 +483,40 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { baseItemFrame.origin.y = containerHeight - contentHeight + floor((contentHeight - itemSize) / 2.0) + itemSize + 4.0 - updatedSize } - nextX += currentItemSize + itemSpacing + nextX += currentItemSize + effectiveItemSpacing + + if i >= topVisibleItems { + if let itemNode = self.visibleItemNodes[i] { + self.visibleItemNodes.removeValue(forKey: i) + + if self.disappearingVisibleItemNodes[i] == nil { + self.disappearingVisibleItemNodes[i] = itemNode + itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak self, weak itemNode] _ in + guard let strongSelf = self, let itemNode = itemNode else { + return + } + itemNode.removeFromSupernode() + if strongSelf.disappearingVisibleItemNodes[i] === itemNode { + strongSelf.disappearingVisibleItemNodes.removeValue(forKey: i) + } + }) + itemNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.1, removeOnCompletion: false) + } + } + } + + if i >= topVisibleItems { + if let itemNode = self.disappearingVisibleItemNodes[i] { + transition.updatePosition(node: itemNode, position: baseItemFrame.center, beginWithCurrentState: true) + } + + break + } if appearBounds.intersects(baseItemFrame) || (self.visibleItemNodes[i] != nil && visibleBounds.intersects(baseItemFrame)) { validIndices.insert(i) - var itemFrame = baseItemFrame - itemFrame.origin.x -= self.horizontalExpandDistance + let itemFrame = baseItemFrame var isPreviewing = false if let highlightedReaction = self.highlightedReaction, highlightedReaction == self.items[i].reaction { @@ -536,10 +590,19 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } if let expandItemView = self.expandItemView { - let baseNextFrame = CGRect(origin: CGPoint(x: nextX + 3.0, y: containerHeight - contentHeight + floor((contentHeight - 30.0) / 2.0) + (self.isExpanded ? 46.0 : 0.0)), size: CGSize(width: 30.0, height: 30.0 + self.extensionDistance)) + let expandItemSize: CGFloat + let expandTintOffset: CGFloat + if self.highlightedReaction != nil { + expandItemSize = floor(30.0 * 0.9) + expandTintOffset = contentHeight - containerHeight + } else { + expandItemSize = 30.0 + expandTintOffset = 0.0 + } + let baseNextFrame = CGRect(origin: CGPoint(x: self.scrollNode.view.bounds.width - expandItemSize - 9.0, y: containerHeight - contentHeight + floor((contentHeight - expandItemSize) / 2.0) + (self.isExpanded ? 46.0 : 0.0)), size: CGSize(width: expandItemSize, height: expandItemSize + self.extensionDistance)) transition.updateFrame(view: expandItemView, frame: baseNextFrame) - transition.updateFrame(view: expandItemView.tintView, frame: baseNextFrame) + transition.updateFrame(view: expandItemView.tintView, frame: baseNextFrame.offsetBy(dx: 0.0, dy: expandTintOffset)) expandItemView.update(size: baseNextFrame.size, transition: transition) } @@ -1108,9 +1171,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, largeExpanded: self.didTriggerExpandedReaction, isPreviewing: false, transition: transition) let additionalAnimationNode: DefaultAnimatedStickerNodeImpl? + var genericAnimationView: AnimationView? let additionalAnimation: TelegramMediaFile? - if self.didTriggerExpandedReaction, !switchToInlineImmediately { + if self.didTriggerExpandedReaction { additionalAnimation = itemNode.item.largeApplicationAnimation } else { additionalAnimation = itemNode.item.applicationAnimation @@ -1129,6 +1193,53 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { additionalAnimationNodeValue.frame = effectFrame additionalAnimationNodeValue.updateLayout(size: effectFrame.size) self.addSubnode(additionalAnimationNodeValue) + } else if itemNode.item.isCustom { + additionalAnimationNode = nil + + if let url = getAppBundle().url(forResource: "generic_reaction_effect", withExtension: "json"), let composition = Animation.filepath(url.path) { + let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) + view.animationSpeed = 1.0 + view.backgroundColor = nil + view.isOpaque = false + + if incomingMessage { + view.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + } + + genericAnimationView = view + + let animationCache = AnimationCacheImpl(basePath: itemNode.context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + let animationRenderer = MultiAnimationRendererImpl() + + let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "BODY 1 Precomp")) + for animationLayer in allLayers { + let baseItemLayer = InlineStickerItemLayer( + context: itemNode.context, + attemptSynchronousLoad: false, + emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: itemNode.item.listAnimation.fileId.id, file: itemNode.item.listAnimation), + file: itemNode.item.listAnimation, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: UIColor(white: 0.0, alpha: 0.0), + pointSize: CGSize(width: 32.0, height: 32.0) + ) + + if let sublayers = animationLayer.sublayers { + for sublayer in sublayers { + sublayer.isHidden = true + } + } + + baseItemLayer.isVisibleForAnimations = true + baseItemLayer.frame = CGRect(origin: CGPoint(x: -0.0, y: -0.0), size: CGSize(width: 500.0, height: 500.0)) + animationLayer.addSublayer(baseItemLayer) + } + + view.frame = effectFrame.insetBy(dx: -10.0, dy: -10.0).offsetBy(dx: incomingMessage ? 22.0 : -22.0, dy: 0.0) + self.view.addSubview(view) + } } else { additionalAnimationNode = nil } @@ -1146,6 +1257,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { additionalAnimationCompleted = true intermediateCompletion() } + } else if let genericAnimationView = genericAnimationView { + genericAnimationView.play(completion: { _ in + additionalAnimationCompleted = true + intermediateCompletion() + }) } else { additionalAnimationCompleted = true } @@ -1172,6 +1288,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if switchToInlineImmediately { targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) + itemNode.isHidden = true } else { targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate) targetView.addSubnode(itemNode) @@ -1342,6 +1459,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { public func expand() { self.animateFromExtensionDistance = self.extensionDistance self.extensionDistance = 0.0 + self.visibleExtensionDistance = 0.0 self.currentContentHeight = 300.0 self.isExpanded = true self.isExpandedUpdated(.animated(duration: 0.4, curve: .spring)) @@ -1585,6 +1703,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } let additionalAnimationNode: AnimatedStickerNode? + var genericAnimationView: AnimationView? if let additionalAnimation = additionalAnimation { let additionalAnimationNodeValue: AnimatedStickerNode @@ -1611,6 +1730,53 @@ public final class StandaloneReactionAnimation: ASDisplayNode { additionalAnimationNodeValue.frame = effectFrame additionalAnimationNodeValue.updateLayout(size: effectFrame.size) self.addSubnode(additionalAnimationNodeValue) + } else if itemNode.item.isCustom { + additionalAnimationNode = nil + + if let url = getAppBundle().url(forResource: "generic_reaction_effect", withExtension: "json"), let composition = Animation.filepath(url.path) { + let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) + view.animationSpeed = 1.0 + view.backgroundColor = nil + view.isOpaque = false + + if incomingMessage { + view.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + } + + genericAnimationView = view + + let animationCache = AnimationCacheImpl(basePath: itemNode.context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + let animationRenderer = MultiAnimationRendererImpl() + + let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "BODY 1 Precomp")) + for animationLayer in allLayers { + let baseItemLayer = InlineStickerItemLayer( + context: itemNode.context, + attemptSynchronousLoad: false, + emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: itemNode.item.listAnimation.fileId.id, file: itemNode.item.listAnimation), + file: itemNode.item.listAnimation, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: UIColor(white: 0.0, alpha: 0.0), + pointSize: CGSize(width: 32.0, height: 32.0) + ) + + if let sublayers = animationLayer.sublayers { + for sublayer in sublayers { + sublayer.isHidden = true + } + } + + baseItemLayer.isVisibleForAnimations = true + baseItemLayer.frame = CGRect(origin: CGPoint(x: -0.0, y: -0.0), size: CGSize(width: 500.0, height: 500.0)) + animationLayer.addSublayer(baseItemLayer) + } + + view.frame = effectFrame.insetBy(dx: -10.0, dy: -10.0).offsetBy(dx: incomingMessage ? 22.0 : -22.0, dy: 0.0) + self.view.addSubview(view) + } } else { additionalAnimationNode = nil } @@ -1762,6 +1928,18 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } additionalAnimationNode.visibility = true + } else if let genericAnimationView = genericAnimationView { + genericAnimationView.play(completion: { _ in + additionalAnimationNode?.alpha = 0.0 + additionalAnimationNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + additionalAnimationCompleted = true + intermediateCompletion() + if forceSmallEffectAnimation { + maybeBeginDismissAnimation() + } else { + beginDismissAnimation() + } + }) } else { additionalAnimationCompleted = true } diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 2f267b99a7..860ef82238 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -13,6 +13,8 @@ import WallpaperResources import LegacyComponents import ItemListUI import WallpaperBackgroundNode +import AnimationCache +import MultiAnimationRenderer private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -34,6 +36,9 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView private var presentationThemeSettings: PresentationThemeSettings private var presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + private let referenceTimestamp: Int32 private let scrollNode: ASScrollNode @@ -58,6 +63,11 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationThemeSettings = presentationThemeSettings + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + let calendar = Calendar(identifier: .gregorian) var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date()) components.hour = 13 @@ -210,7 +220,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) { var items: [ChatListItem] = [] - let interaction = ChatListNodeInteraction(context: self.context, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index 8afed86d68..b1a08141c1 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -12,6 +12,8 @@ import AccountContext import WallpaperResources import PresentationDataUtils import WallpaperBackgroundNode +import AnimationCache +import MultiAnimationRenderer private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -152,6 +154,9 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate private let mode: ThemeAccentColorControllerMode private var presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + private let ready: Promise private let queue = Queue() @@ -228,6 +233,11 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate self.wallpaper = self.presentationData.chatWallpaper let bubbleCorners = self.presentationData.chatBubbleCorners + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + self.ready = ready let calendar = Calendar(identifier: .gregorian) @@ -830,7 +840,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) { var items: [ChatListItem] = [] - let interaction = ChatListNodeInteraction(context: self.context, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 4377e348ee..d3c2b0a811 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -12,6 +12,8 @@ import ChatListUI import WallpaperResources import LegacyComponents import WallpaperBackgroundNode +import AnimationCache +import MultiAnimationRenderer private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -33,6 +35,9 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { private var previewTheme: PresentationTheme private var presentationData: PresentationData private let isPreview: Bool + + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let ready: Promise @@ -83,6 +88,11 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + let calendar = Calendar(identifier: .gregorian) var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date()) components.hour = 13 @@ -354,7 +364,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) { var items: [ChatListItem] = [] - let interaction = ChatListNodeInteraction(context: self.context, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture, _ in gesture?.cancel() diff --git a/submodules/ShareController/Sources/ShareSearchContainerNode.swift b/submodules/ShareController/Sources/ShareSearchContainerNode.swift index 78d8ef9720..843274ac68 100644 --- a/submodules/ShareController/Sources/ShareSearchContainerNode.swift +++ b/submodules/ShareController/Sources/ShareSearchContainerNode.swift @@ -93,7 +93,7 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable { if let associatedPeer = associatedPeer { peers[associatedPeer.id] = associatedPeer } - let peer = EngineRenderedPeer(RenderedPeer(peerId: peer.id, peers: SimpleDictionary(peers))) + let peer = EngineRenderedPeer(RenderedPeer(peerId: peer.id, peers: SimpleDictionary(peers), associatedMedia: [:])) return ShareControllerPeerGridItem(context: context, theme: theme, strings: strings, peer: peer, presence: presence, controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true) } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 4394e5e1ed..f8fb74f316 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1258,6 +1258,14 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo return peer } }) + case let .updateUserEmojiStatus(userId, emojiStatus): + updatedState.updatePeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), { peer in + if let user = peer as? TelegramUser { + return user.withUpdatedEmojiStatus(PeerEmojiStatus(apiStatus: emojiStatus)) + } else { + return peer + } + }) case let .updatePeerSettings(peer, settings): let peerStatusSettings = PeerStatusSettings(apiSettings: settings) updatedState.updateCachedPeerData(peer.peerId, { current in diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift index 3e9a07c928..031c3b2915 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift @@ -159,6 +159,24 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { return self.recentPeers.map(\.peerId) } + public var associatedMediaIds: [MediaId] { + var result: [MediaId] = [] + + for reaction in self.reactions { + switch reaction.value { + case .builtin: + break + case let .custom(fileId): + let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) + if !result.contains(mediaId) { + result.append(mediaId) + } + } + } + + return result + } + public init(canViewList: Bool, reactions: [MessageReaction], recentPeers: [RecentPeer]) { self.canViewList = canViewList self.reactions = reactions @@ -241,6 +259,24 @@ public final class PendingReactionsMessageAttribute: MessageAttribute { } } + public var associatedMediaIds: [MediaId] { + var result: [MediaId] = [] + + for reaction in self.reactions { + switch reaction.value { + case .builtin: + break + case let .custom(fileId): + let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) + if !result.contains(mediaId) { + result.append(mediaId) + } + } + } + + return result + } + public init(accountPeerId: PeerId?, reactions: [PendingReaction], isLarge: Bool) { self.accountPeerId = accountPeerId self.reactions = reactions diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift index 5d76014559..9ed693c616 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift @@ -169,6 +169,8 @@ public final class TelegramChannel: Peer, Equatable { return .title(title: self.title, addressName: self.username) } + public var associatedMediaIds: [MediaId]? { return nil } + public let associatedPeerId: PeerId? = nil public let notificationSettingsPeerId: PeerId? = nil diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramGroup.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramGroup.swift index 66a075bf27..0231ebae13 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramGroup.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramGroup.swift @@ -92,6 +92,8 @@ public final class TelegramGroup: Peer, Equatable { return .title(title: self.title, addressName: nil) } + public var associatedMediaIds: [MediaId]? { return nil } + public let associatedPeerId: PeerId? = nil public let notificationSettingsPeerId: PeerId? = nil diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift index d5a5301ea8..a0df67bac2 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift @@ -14,6 +14,8 @@ public final class TelegramSecretChat: Peer, Equatable { return .title(title: "", addressName: nil) } + public var associatedMediaIds: [MediaId]? { return nil } + public let associatedPeerId: PeerId? public let notificationSettingsPeerId: PeerId? diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index 96bf0ff470..bee75b9f92 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -104,6 +104,14 @@ public final class TelegramUser: Peer, Equatable { return .personName(first: self.firstName ?? "", last: self.lastName ?? "", addressName: self.username, phoneNumber: self.phone) } + public var associatedMediaIds: [MediaId]? { + if let emojiStatus = self.emojiStatus { + return [MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId)] + } else { + return nil + } + } + public let associatedPeerId: PeerId? = nil public let notificationSettingsPeerId: PeerId? = nil diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 047fefea4e..c639f80a65 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -188,7 +188,7 @@ public extension TelegramEngine.EngineData.Item { peers[mainPeer.id] = EnginePeer(mainPeer) } - return EngineRenderedPeer(peerId: self.id, peers: peers) + return EngineRenderedPeer(peerId: self.id, peers: peers, associatedMedia: view.media) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index f7a32199e3..973605f3e4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -531,15 +531,18 @@ public extension EnginePeer { public final class EngineRenderedPeer: Equatable { public let peerId: EnginePeer.Id public let peers: [EnginePeer.Id: EnginePeer] + public let associatedMedia: [EngineMedia.Id: Media] - public init(peerId: EnginePeer.Id, peers: [EnginePeer.Id: EnginePeer]) { + public init(peerId: EnginePeer.Id, peers: [EnginePeer.Id: EnginePeer], associatedMedia: [EngineMedia.Id: Media]) { self.peerId = peerId self.peers = peers + self.associatedMedia = associatedMedia } public init(peer: EnginePeer) { self.peerId = peer.id self.peers = [peer.id: peer] + self.associatedMedia = [:] } public static func ==(lhs: EngineRenderedPeer, rhs: EngineRenderedPeer) -> Bool { @@ -549,6 +552,9 @@ public final class EngineRenderedPeer: Equatable { if lhs.peers != rhs.peers { return false } + if !areMediaDictionariesEqual(lhs.associatedMedia, rhs.associatedMedia) { + return false + } return true } @@ -575,7 +581,7 @@ public extension EngineRenderedPeer { for (id, peer) in renderedPeer.peers { mappedPeers[id] = EnginePeer(peer) } - self.init(peerId: renderedPeer.peerId, peers: mappedPeers) + self.init(peerId: renderedPeer.peerId, peers: mappedPeers, associatedMedia: renderedPeer.associatedMedia) } convenience init(message: EngineMessage) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift index a3e7e1ff4e..f611f03399 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift @@ -87,7 +87,7 @@ func _internal_recentlySearchedPeers(postbox: Postbox) -> Signal<[RecentlySearch subpeerSummary = RecentlySearchedPeerSubpeerSummary(count: Int(count)) } - result.append(RecentlySearchedPeer(peer: RenderedPeer(peerId: peerId, peers: SimpleDictionary(peerView.peers)), presence: presence, notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, unreadCount: unreadCount, subpeerSummary: subpeerSummary)) + result.append(RecentlySearchedPeer(peer: RenderedPeer(peerId: peerId, peers: SimpleDictionary(peerView.peers), associatedMedia: peerView.media), presence: presence, notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, unreadCount: unreadCount, subpeerSummary: subpeerSummary)) } } diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 31207ee4b0..aac2589ae8 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -250,7 +250,7 @@ public extension RenderedPeer { } } } - self.init(peerId: message.id.peerId, peers: peers) + self.init(peerId: message.id.peerId, peers: peers, associatedMedia: [:]) } var chatMainPeer: Peer? { diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index 4faec44345..bc8a85f097 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -110,70 +110,6 @@ public final class EmojiStatusComponent: Component { var emojiPlaceholderColor: UIColor? var emojiSize = CGSize() - /* - if case .fake = credibilityIcon { - image = PresentationResourcesChatList.fakeIcon(presentationData.theme, strings: presentationData.strings, type: .regular) - } else if case .scam = credibilityIcon { - image = PresentationResourcesChatList.scamIcon(presentationData.theme, strings: presentationData.strings, type: .regular) - } else if case .verified = credibilityIcon { - if let backgroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconForeground") { - image = generateImage(backgroundImage.size, contextGenerator: { size, context in - if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.saveGState() - context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) - - context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.restoreGState() - - context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) - context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - expandedImage = generateImage(backgroundImage.size, contextGenerator: { size, context in - if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.saveGState() - context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) - context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.restoreGState() - - context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) - context.setBlendMode(.clear) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - } else { - image = nil - } - } else if case .premium = credibilityIcon { - if let sourceImage = UIImage(bundleImageName: "Peer Info/PremiumIcon") { - image = generateImage(sourceImage.size, contextGenerator: { size, context in - if let cgImage = sourceImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) - - context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - expandedImage = generateImage(sourceImage.size, contextGenerator: { size, context in - if let cgImage = sourceImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) - context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - } else { - image = nil - } - } - */ - if self.component?.content != component.content { switch component.content { case .none: @@ -232,10 +168,15 @@ public final class EmojiStatusComponent: Component { if let animationLayer = self.animationLayer { self.animationLayer = nil - animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak animationLayer] _ in - animationLayer?.removeFromSuperlayer() - }) - animationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + + if !transition.animation.isImmediate { + animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak animationLayer] _ in + animationLayer?.removeFromSuperlayer() + }) + animationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + animationLayer.removeFromSuperlayer() + } } } } @@ -262,8 +203,10 @@ public final class EmojiStatusComponent: Component { self.iconView = iconView self.addSubview(iconView) - iconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - iconView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + if !transition.animation.isImmediate { + iconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + iconView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } } iconView.image = iconImage size = iconImage.size.aspectFilled(availableSize) @@ -272,10 +215,14 @@ public final class EmojiStatusComponent: Component { if let iconView = self.iconView { self.iconView = nil - iconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak iconView] _ in - iconView?.removeFromSuperview() - }) - iconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + if !transition.animation.isImmediate { + iconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak iconView] _ in + iconView?.removeFromSuperview() + }) + iconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + iconView.removeFromSuperview() + } } } @@ -304,8 +251,10 @@ public final class EmojiStatusComponent: Component { self.animationLayer = animationLayer self.layer.addSublayer(animationLayer) - animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - animationLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + if !transition.animation.isImmediate { + animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + animationLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } } animationLayer.frame = CGRect(origin: CGPoint(), size: size) animationLayer.isVisibleForAnimations = true @@ -317,7 +266,7 @@ public final class EmojiStatusComponent: Component { return } strongSelf.emojiFile = result[emojiFileId] - strongSelf.state?.updated(transition: .immediate) + strongSelf.state?.updated(transition: transition) emojiFileUpdated?(result[emojiFileId]) }) @@ -335,10 +284,14 @@ public final class EmojiStatusComponent: Component { if let animationLayer = self.animationLayer { self.animationLayer = nil - animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak animationLayer] _ in - animationLayer?.removeFromSuperlayer() - }) - animationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + if !transition.animation.isImmediate { + animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak animationLayer] _ in + animationLayer?.removeFromSuperlayer() + }) + animationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + animationLayer.removeFromSuperlayer() + } } } diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD index f5c23070fc..6e7bc8c658 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD @@ -12,6 +12,8 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/ComponentFlow:ComponentFlow", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 7e5a698e76..e4b0ee2797 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -11,6 +11,8 @@ import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import PagerComponent +import Postbox +import TelegramCore public final class EmojiStatusSelectionComponent: Component { public typealias EnvironmentType = Empty @@ -180,6 +182,7 @@ public final class EmojiStatusSelectionController: ViewController { private var emojiContentDisposable: Disposable? private var emojiContent: EmojiPagerContentComponent? + private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private var isDismissed: Bool = false @@ -245,7 +248,28 @@ public final class EmojiStatusSelectionController: ViewController { openFeatured: { }, addGroupAction: { groupId, isPremiumLocked in + guard let strongSelf = self, let collectionId = groupId.base as? ItemCollectionId else { + return + } + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) + let _ = (strongSelf.context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let strongSelf = self, let view = views.views[viewKey] as? OrderedItemListView else { + return + } + for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + if featuredEmojiPack.info.id == collectionId { + if let strongSelf = self { + strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId)) + } + let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() + + break + } + } + }) }, clearGroup: { groupId in }, @@ -295,6 +319,8 @@ public final class EmojiStatusSelectionController: ViewController { func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) { self.validLayout = layout + var transition = transition + guard let emojiContent = self.emojiContent else { return } @@ -320,6 +346,12 @@ public final class EmojiStatusSelectionController: ViewController { let sideInset: CGFloat = 16.0 + if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint { + self.scheduledEmojiContentAnimationHint = nil + let contentAnimation = scheduledEmojiContentAnimationHint + transition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) + } + let componentSize = self.componentHost.update( transition: transition, component: AnyComponent(EmojiStatusSelectionComponent( diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 541577f16f..ffe2f7c731 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -27,6 +27,27 @@ private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bund private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white) private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white) +private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = { + guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else { + return [] + } + guard let string = try? String(contentsOf: URL(fileURLWithPath: path)) else { + return [] + } + + var result: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = [] + + let orderedSegments = EmojiPagerContentComponent.StaticEmojiSegment.allCases + + let segments = string.components(separatedBy: "\n\n") + for i in 0 ..< min(segments.count, orderedSegments.count) { + let list = segments[i].components(separatedBy: " ") + result.append((orderedSegments[i], list)) + } + + return result +}() + private final class WarpView: UIView { private final class WarpPartView: UIView { let cloneView: PortalView @@ -2662,6 +2683,8 @@ public final class EmojiPagerContentComponent: Component { continue } if let sourceItem = sourceItems[file.fileId] { + itemLayer.animatePosition(from: CGPoint(x: sourceItem.position.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + component.animationRenderer.setFrameIndex(itemId: animationData.resource.resource.id.stringRepresentation, size: itemLayer.pixelSize, frameIndex: sourceItem.frameIndex, placeholder: sourceItem.placeholder) } else { let distance = itemLayer.position.y - itemLayout.frame(groupIndex: 0, itemIndex: 0).midY @@ -3433,7 +3456,7 @@ public final class EmojiPagerContentComponent: Component { let groupBorderRadius: CGFloat = 16.0 - if itemGroup.isPremiumLocked && !itemGroup.isFeatured && !itemGroup.isEmbedded { + if itemGroup.isPremiumLocked && !itemGroup.isFeatured && !itemGroup.isEmbedded && !itemLayout.curveNearBounds { validGroupBorderIds.insert(itemGroup.groupId) let groupBorderLayer: GroupBorderLayer var groupBorderTransition = transition @@ -4414,4 +4437,397 @@ public final class EmojiPagerContentComponent: Component { 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) } + + private static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal { + let hasPremium: Signal + if premiumIfSavedMessages, let chatPeerId = chatPeerId, chatPeerId == context.account.peerId { + hasPremium = .single(true) + } else { + 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 + } + return hasPremium + } + + public static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, isStatusSelection: Bool, isReactionSelection: Bool, reactionItems: [AvailableReactions.Reaction], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?) -> Signal { + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let isPremiumDisabled = premiumConfiguration.isPremiumDisabled + + let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings + + var orderedItemListCollectionIds: [Int32] = [] + + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.LocalRecentEmoji) + + if isStatusSelection { + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji) + } + + let emojiItems: Signal = combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), + hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true), + context.account.viewTracker.featuredEmojiPacks() + ) + |> map { view, hasPremium, featuredEmojiPacks -> EmojiPagerContentComponent in + struct ItemGroup { + var supergroupId: AnyHashable + var id: AnyHashable + var title: String? + var subtitle: String? + var isPremiumLocked: Bool + var isFeatured: Bool + var isExpandable: Bool + var headerItem: EntityKeyboardAnimationData? + var items: [EmojiPagerContentComponent.Item] + } + var itemGroups: [ItemGroup] = [] + var itemGroupIndexById: [AnyHashable: Int] = [:] + + var recentEmoji: OrderedItemListView? + var featuredStatusEmoji: OrderedItemListView? + var recentStatusEmoji: OrderedItemListView? + for orderedView in view.orderedItemListsViews { + if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji { + recentEmoji = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedStatusEmoji { + featuredStatusEmoji = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStatusEmoji { + recentStatusEmoji = orderedView + } + } + + if isStatusSelection { + let resultItem = EmojiPagerContentComponent.Item( + animationData: nil, + content: .icon(.premiumStar), + itemFile: nil, + subgroupId: nil + ) + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) + } + + var existingIds = Set() + if let recentStatusEmoji = recentStatusEmoji { + for item in recentStatusEmoji.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + + let file = item.media + if existingIds.contains(file.fileId) { + continue + } + existingIds.insert(file.fileId) + + let resultItem: EmojiPagerContentComponent.Item + + let animationData = EntityKeyboardAnimationData(file: file) + resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: file, + subgroupId: nil + ) + + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } + } + } + if let featuredStatusEmoji = featuredStatusEmoji { + for item in featuredStatusEmoji.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + + let file = item.media + if existingIds.contains(file.fileId) { + continue + } + existingIds.insert(file.fileId) + + let resultItem: EmojiPagerContentComponent.Item + + let animationData = EntityKeyboardAnimationData(file: file) + resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: file, + subgroupId: nil + ) + + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } + } + } + } else if isReactionSelection { + for reactionItem in reactionItems { + let animationFile = reactionItem.selectAnimation + let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: animationFile, + subgroupId: nil + ) + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) + } + } + } + + if let recentEmoji = recentEmoji, !isReactionSelection, !isStatusSelection { + for item in recentEmoji.items { + guard let item = item.contents.get(RecentEmojiItem.self) else { + continue + } + + if case let .file(file) = item.content, isPremiumDisabled, file.isPremiumEmoji { + continue + } + + if !areCustomEmojiEnabled, case .file = item.content { + continue + } + + let resultItem: EmojiPagerContentComponent.Item + switch item.content { + case let .file(file): + let animationData = EntityKeyboardAnimationData(file: file) + resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: file, + subgroupId: nil + ) + case let .text(text): + resultItem = EmojiPagerContentComponent.Item( + animationData: nil, + content: .staticEmoji(text), + itemFile: nil, + subgroupId: nil + ) + } + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Emoji_FrequentlyUsed, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) + } + } + } + + if areUnicodeEmojiEnabled { + for (subgroupId, list) in staticEmojiMapping { + let groupId: AnyHashable = "static" + for emojiString in list { + let resultItem = EmojiPagerContentComponent.Item( + animationData: nil, + content: .staticEmoji(emojiString), + itemFile: nil, + subgroupId: subgroupId.rawValue + ) + + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleEmoji, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) + } + } + } + } + + var installedCollectionIds = Set() + for (id, _, _) in view.collectionInfos { + installedCollectionIds.insert(id) + } + + if areCustomEmojiEnabled { + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil + ) + + let supergroupId = entry.index.collectionId + let groupId: AnyHashable = supergroupId + let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium + if isPremiumLocked && isPremiumDisabled { + continue + } + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + + var title = "" + var headerItem: EntityKeyboardAnimationData? + inner: for (id, info, _) in view.collectionInfos { + if id == entry.index.collectionId, let info = info as? StickerPackCollectionInfo { + title = info.title + + if let thumbnail = info.thumbnail { + let type: EntityKeyboardAnimationData.ItemType + if item.file.isAnimatedSticker { + type = .lottie + } else if item.file.isVideoEmoji || item.file.isVideoSticker { + type = .video + } else { + type = .still + } + + headerItem = EntityKeyboardAnimationData( + id: .stickerPackThumbnail(info.id), + type: type, + resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), + dimensions: thumbnail.dimensions.cgSize, + immediateThumbnailData: info.immediateThumbnailData, + isReaction: false + ) + } + + break inner + } + } + itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: false, isExpandable: false, headerItem: headerItem, items: [resultItem])) + } + } + + if !isStandalone { + for featuredEmojiPack in featuredEmojiPacks { + if installedCollectionIds.contains(featuredEmojiPack.info.id) { + continue + } + + for item in featuredEmojiPack.topItems { + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil + ) + + let supergroupId = featuredEmojiPack.info.id + let groupId: AnyHashable = supergroupId + let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium + if isPremiumLocked && isPremiumDisabled { + continue + } + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + + var headerItem: EntityKeyboardAnimationData? + if let thumbnailFileId = featuredEmojiPack.info.thumbnailFileId, let file = featuredEmojiPack.topItems.first(where: { $0.file.fileId.id == thumbnailFileId }) { + headerItem = EntityKeyboardAnimationData(file: file.file) + } else if let thumbnail = featuredEmojiPack.info.thumbnail { + let info = featuredEmojiPack.info + let type: EntityKeyboardAnimationData.ItemType + if item.file.isAnimatedSticker { + type = .lottie + } else if item.file.isVideoEmoji || item.file.isVideoSticker { + type = .video + } else { + type = .still + } + + headerItem = EntityKeyboardAnimationData( + id: .stickerPackThumbnail(info.id), + type: type, + resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), + dimensions: thumbnail.dimensions.cgSize, + immediateThumbnailData: info.immediateThumbnailData, + isReaction: false + ) + } + + itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: true, isExpandable: true, headerItem: headerItem, items: [resultItem])) + } + } + } + } + } + + return EmojiPagerContentComponent( + id: "emoji", + context: context, + avatarPeer: nil, + animationCache: animationCache, + animationRenderer: animationRenderer, + inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), + itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in + var hasClear = false + if group.id == AnyHashable("recent") { + hasClear = true + } + + var headerItem = group.headerItem + + if let groupId = group.id.base as? ItemCollectionId { + outer: for (id, info, _) in view.collectionInfos { + if id == groupId, let info = info as? StickerPackCollectionInfo { + if let thumbnailFileId = info.thumbnailFileId { + for item in group.items { + if let itemFile = item.itemFile, itemFile.fileId.id == thumbnailFileId { + headerItem = EntityKeyboardAnimationData(file: itemFile) + break outer + } + } + } + } + } + } + + return EmojiPagerContentComponent.ItemGroup( + supergroupId: group.supergroupId, + groupId: group.id, + title: group.title, + subtitle: group.subtitle, + actionButtonTitle: nil, + isFeatured: group.isFeatured, + isPremiumLocked: group.isPremiumLocked, + isEmbedded: false, + hasClear: hasClear, + isExpandable: group.isExpandable, + displayPremiumBadges: false, + headerItem: headerItem, + items: group.items + ) + }, + itemLayoutType: .compact, + warpContentsOnEdges: isReactionSelection || isStatusSelection + ) + } + return emojiItems + } } diff --git a/submodules/TelegramUI/Resources/Animations/generic_reaction_effect.json b/submodules/TelegramUI/Resources/Animations/generic_reaction_effect.json new file mode 100644 index 0000000000..d83c453b18 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/generic_reaction_effect.json @@ -0,0 +1 @@ +{"tgs":1,"v":"5.5.2","fr":60,"ip":0,"op":180,"w":512,"h":512,"nm":"MAIN","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"BODY 1","sr":1,"ks":{"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[625.68,1067.113,0]},"s":{"a":0,"k":[103,103,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[9.565,25.371],[2.68,0],[7.788,-20.656],[-3.679,11.745],[-2.247,0],[-7.411,-23.657]],"o":[[-7.788,-20.656],[-2.523,0],[-9.566,25.371],[7.411,-23.657],[2.369,0],[3.679,11.745]],"v":[[-30.474,-49.257],[-55.518,-70.085],[-80.562,-49.257],[-77.645,-8.114],[-55.518,-31.514],[-33.391,-8.114]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.490196079016,0.184313729405,0.054901961237,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[677.956,1035.621]},"a":{"a":0,"k":[-53.701,-40.574]},"s":{"a":0,"k":[-100.903,99.097]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Eye R","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[9.849,25.371],[2.76,0],[8.018,-20.656],[-3.788,11.745],[-2.313,0],[-7.63,-23.657]],"o":[[-8.018,-20.656],[-2.598,0],[-9.849,25.371],[7.63,-23.657],[2.439,0],[3.788,11.745]],"v":[[-35.931,-49.257],[-61.717,-70.085],[-87.502,-49.257],[-84.499,-8.114],[-61.717,-31.514],[-38.935,-8.114]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.490196079016,0.184313729405,0.054901961237,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[569.584,1035.621]},"a":{"a":0,"k":[-53.701,-40.574]},"s":{"a":0,"k":[100.903,99.097]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Eye L","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[625.426,1013.075]},"a":{"a":0,"k":[623.77,1035.621]},"s":{"a":0,"k":[97.549,102.52]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Eyes 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[11.985,-45.005],[25.226,15.902],[-91.092,-1.542]],"o":[[-74.879,21.928],[-19.984,-49.554],[93.26,-1.299]],"v":[[114.577,61.938],[-107.95,61.566],[4.023,43.496]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.960784316063,0.960784316063,0.960784316063,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-70.614,14.062],[36.502,0.124],[22.149,25.985]],"o":[[-22.963,26.242],[-39.998,-0.136],[36.996,11.903]],"v":[[102.692,78.663],[4.034,123.112],[-96.354,78.362]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.960784316063,0.960784316063,0.960784316063,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 5","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[0.808,71.626]},"a":{"a":0,"k":[0.808,71.626]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"teeth","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.008,-0.575],[70.215,1.793],[-0.044,0.501],[-98.186,-2.118],[-1.296,-16.564]],"o":[[0.718,56.868],[-124.484,-3.2],[1.355,-16.348],[97.907,-2.066],[0.037,0.501]],"v":[[133.241,28.393],[2.85,134.725],[-126.987,28.005],[3.506,32.923],[133.286,27.996]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.490196079016,0.184313729405,0.054901961237,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 7","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[626.224,1134.199]},"a":{"a":0,"k":[0.811,78.649]},"s":{"a":0,"k":[98.43,101.594]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Mouth 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,-104.313],[5.311,-17.436],[31.05,-23.708],[43.057,0],[31.47,23.457],[12.414,36.634],[0,21.217],[-104.313,0]],"o":[[0,19.173],[-11.657,38.27],[-31.769,24.257],[-42.268,0],[-30.398,-22.658],[-6.451,-19.037],[0,-104.313],[104.313,0]],"v":[[667.465,-59.252],[659.298,-4.138],[593.109,90.958],[478.59,129.623],[365.826,92.282],[299.664,1.402],[289.715,-59.252],[478.59,-248.128]],"c":true}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.945098039216,0.39109287636,0.083761521882,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.811764717102,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[-476.698,50.455]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[624.59,1076.875]},"a":{"a":0,"k":[0.813,1.056]},"s":{"a":0,"k":[100.925,99.075]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"BODY 2","bm":0,"hd":false}],"ip":0,"op":184,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL SCALE ALL","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[426.595,253.107,0]},"a":{"a":0,"k":[50,50,0]},"s":{"a":0,"k":[99,99,100]}},"ao":0,"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.207,"y":0.684},"o":{"x":0.05,"y":0},"t":0,"s":[46.316,9.099,0],"to":[-8.016,-28.795,0],"ti":[22.744,3.645,0]},{"i":{"x":0.787,"y":1},"o":{"x":0.6,"y":0.157},"t":13,"s":[-3.625,-57.882,0],"to":[-42.295,2.68,0],"ti":[0,0,0]},{"t":55,"s":[-58.955,361.868,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.631,0.631,0.564],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":0,"s":[-14,14,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":43,"s":[-19,19,100]},{"t":53,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":2,"op":48,"st":1,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.32,"y":0.763},"o":{"x":0.05,"y":0},"t":-1,"s":[51.425,5.3,0],"to":[0,-1.667,0],"ti":[-8.263,0.945,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.416,"y":0.132},"t":7,"s":[68.425,-66.7,0],"to":[17.562,-2.009,0],"ti":[-6.898,-120.235,0]},{"t":42,"s":[102.661,289.307,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.156,0.156,0.156],"y":[0,0,0]},"t":-1,"s":[7,7,100]},{"i":{"x":[0.718,0.718,0.718],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":14,"s":[20,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":32,"s":[20,20,100]},{"t":45,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":2,"op":41,"st":-1,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.499,"y":0.705},"o":{"x":0.05,"y":0},"t":-1,"s":[26.99,1.903,0],"to":[-15.295,-19.9,0],"ti":[19.777,-12.154,0]},{"i":{"x":0.823,"y":1},"o":{"x":0.64,"y":0.44},"t":7,"s":[-57.173,-1.605,0],"to":[-62.741,38.557,0],"ti":[11.61,-102.129,0]},{"t":42,"s":[-161.339,310.307,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":-1,"s":[10,10,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":32,"s":[22,22,100]},{"t":43,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":1,"op":38,"st":-1,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.201,"y":0.933},"o":{"x":0.1,"y":0},"t":-1,"s":[37.454,-10.666,0],"to":[1.659,-26.594,0],"ti":[-13.916,0.231,0]},{"i":{"x":0.806,"y":0.637},"o":{"x":0.64,"y":0.062},"t":10,"s":[55.218,-138.248,0],"to":[18.497,-0.306,0],"ti":[-0.07,-88.869,0]},{"t":52,"s":[75.838,288.611,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.646,0.646,0.43],"y":[1,1,1]},"o":{"x":[0.577,0.577,0.235],"y":[0.168,0.168,0]},"t":-1,"s":[10,10,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":44,"s":[23,23,100]},{"t":55,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":1,"op":51,"st":-1,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.283,"y":0.718},"o":{"x":0.11,"y":0},"t":-1,"s":[56.059,29.159,0],"to":[-53.493,-80.535,0],"ti":[60.825,-1.431,0]},{"i":{"x":0.806,"y":1},"o":{"x":0.6,"y":0.407},"t":11,"s":[-141.735,-119.127,0],"to":[-113.9,2.68,0],"ti":[0,0,0]},{"t":49,"s":[-333.655,288.733,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.432,0.432,0.432],"y":[1,1,1]},"o":{"x":[0.233,0.233,0.233],"y":[0.189,0.189,0]},"t":-1,"s":[9.009,9.009,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":40,"s":[30,30,100]},{"t":51,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":1,"op":46,"st":-1,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.339,"y":0.87},"o":{"x":0.11,"y":0},"t":-2,"s":[72.209,11.097,0],"to":[-28.213,-68.899,0],"ti":[51.566,-5.031,0]},{"i":{"x":0.781,"y":1},"o":{"x":0.7,"y":0.142},"t":15,"s":[-53.342,-167.527,0],"to":[-82.41,8.04,0],"ti":[0,0,0]},{"t":56,"s":[-218.85,330.743,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.61,0.61,0.61],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":-2,"s":[6.806,6.806,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":44,"s":[26,26,100]},{"t":56,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":1,"op":52,"st":-2,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.404,"y":0.702},"o":{"x":0.125,"y":0.16},"t":2,"s":[46.772,16.267,0],"to":[-33.457,-55.77,0],"ti":[103.874,-12.223,0]},{"i":{"x":0.778,"y":1},"o":{"x":0.6,"y":0.339},"t":24,"s":[-172.095,-127.807,0],"to":[-137.203,16.145,0],"ti":[0,0,0]},{"t":60,"s":[-375.485,195.643,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.597,0.597,0.407],"y":[1,1,1]},"o":{"x":[0.05,0.05,0.05],"y":[0,0,0]},"t":2,"s":[-8.062,8.062,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":47,"s":[-18,18,100]},{"t":58,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":2,"op":53,"st":-2,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.296,"y":0.778},"o":{"x":0.05,"y":0},"t":6,"s":[72.305,51.553,0],"to":[0,0,0],"ti":[50.18,1.814,0]},{"i":{"x":0.763,"y":1},"o":{"x":0.6,"y":0.165},"t":31,"s":[-24.575,-168.867,0],"to":[-60.781,-2.198,0],"ti":[0,0,0]},{"t":76,"s":[-136.215,321.293,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.504,0.504,0.4],"y":[1,1,1]},"o":{"x":[0.52,0.52,0.3],"y":[0,0,0]},"t":6,"s":[6.7,6.7,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.135,0.135,0.3],"y":[0,0,0]},"t":62,"s":[24,24,100]},{"t":74,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":10,"op":70,"st":-2,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.3,"y":0.904},"o":{"x":0.1,"y":0},"t":16,"s":[73.605,3.078,0],"to":[0,0,0],"ti":[-12.982,0.005,0]},{"i":{"x":0.764,"y":1},"o":{"x":0.64,"y":0.09},"t":33,"s":[85.232,-149.87,0],"to":[13.057,-0.005,0],"ti":[0,0,0]},{"t":82,"s":[103.005,285.768,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.674,0.674,0.4],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":16,"s":[6.7,6.7,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":71,"s":[16,16,100]},{"t":80,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":16,"op":76,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.317,"y":0.665},"o":{"x":0.15,"y":0},"t":15,"s":[36.62,35.238,0],"to":[-24.12,-59.295,0],"ti":[105,-7.02,0]},{"i":{"x":0.85,"y":1},"o":{"x":0.6,"y":0.27},"t":42,"s":[-161.73,-167.752,0],"to":[-133.741,9.247,0],"ti":[0,0,0]},{"t":84,"s":[-357.525,282.143,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":15,"s":[10,10,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":70,"s":[26,26,100]},{"t":82,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":18,"op":78,"st":-12,"bm":0},{"ddd":0,"ind":12,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.3,"y":0.868},"o":{"x":0.15,"y":0},"t":20,"s":[39.305,8.553,0],"to":[0,0,0],"ti":[12.832,-0.076,0]},{"i":{"x":0.8,"y":1},"o":{"x":0.605,"y":0.102},"t":28,"s":[6.188,-65.857,0],"to":[-20.436,0.122,0],"ti":[0,0,0]},{"t":60,"s":[-19.395,304.073,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.601,0.601,0.601],"y":[1,1,1]},"o":{"x":[0.64,0.64,0.3],"y":[0,0,0]},"t":20,"s":[8,8,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":49,"s":[18,18,100]},{"t":58,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":22,"op":56,"st":-6,"bm":0},{"ddd":0,"ind":13,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.868,"y":1},"o":{"x":0.605,"y":0.159},"t":34,"s":[-146.812,-60.357,0],"to":[-24.783,14.75,0],"ti":[21.8,-144.18,0]},{"t":64,"s":[-270.395,312.073,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.05,0.05,0.05],"y":[0,0,0]},"t":34,"s":[12,12,100]},{"i":{"x":[0.85,0.85,0.85],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":54,"s":[22,22,100]},{"t":64,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":34,"op":62,"st":-5,"bm":0},{"ddd":0,"ind":14,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.4,"y":0.861},"o":{"x":0.15,"y":0},"t":29,"s":[78.63,31.218,0],"to":[-7.46,-21.855,0],"ti":[53.6,0,0]},{"i":{"x":0.85,"y":1},"o":{"x":0.6,"y":0.133},"t":47,"s":[-18.915,-177.747,0],"to":[-95.32,5.38,0],"ti":[14.01,-92.815,0]},{"t":90,"s":[-203.875,371.758,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.603,0.603,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":29,"s":[-6.7,6.7,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":78,"s":[-20,20,100]},{"t":90,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":33,"op":85,"st":-6,"bm":0},{"ddd":0,"ind":15,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.3,"y":0.834},"o":{"x":0.15,"y":0},"t":41,"s":[28.885,17.743,0],"to":[0,0,0],"ti":[-11.557,-2.816,0]},{"i":{"x":0.85,"y":1},"o":{"x":0.64,"y":0.111},"t":59,"s":[41.957,-150.575,0],"to":[11.557,2.816,0],"ti":[-0.07,-88.869,0]},{"t":94,"s":[37.456,300.592,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":41,"s":[6.7,6.7,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":83,"s":[25,25,100]},{"t":94,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":42,"op":91,"st":-17,"bm":0},{"ddd":0,"ind":16,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.283,"y":0.666},"o":{"x":0.11,"y":0},"t":56,"s":[36.059,7.159,0],"to":[-42.493,-99.535,0],"ti":[60.825,-1.431,0]},{"i":{"x":0.806,"y":1},"o":{"x":0.6,"y":0.274},"t":72,"s":[-101.735,-119.168,0],"to":[-113.9,2.68,0],"ti":[0,0,0]},{"t":109,"s":[-301.655,292.733,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.432,0.432,0.432],"y":[1,1,1]},"o":{"x":[0.233,0.233,0.233],"y":[0.214,0.214,0]},"t":56,"s":[9.009,9.009,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":98,"s":[28,28,100]},{"t":110,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":56,"op":104,"st":56,"bm":0},{"ddd":0,"ind":17,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.296,"y":0.794},"o":{"x":0.05,"y":0},"t":57,"s":[72.305,51.553,0],"to":[0,0,0],"ti":[50.18,1.814,0]},{"i":{"x":0.763,"y":1},"o":{"x":0.6,"y":0.151},"t":80,"s":[-16.575,-168.867,0],"to":[-60.781,-2.198,0],"ti":[0,0,0]},{"t":122,"s":[-144.215,327.293,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.504,0.504,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":57,"s":[8,8,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,2.754]},"o":{"x":[0.135,0.135,0.3],"y":[0,0,0]},"t":108,"s":[21,21,100]},{"t":120,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":61,"op":116,"st":49,"bm":0},{"ddd":0,"ind":18,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.32,"y":0.772},"o":{"x":0.05,"y":0},"t":71,"s":[51.425,5.3,0],"to":[0,-1.667,0],"ti":[-8.263,0.945,0]},{"i":{"x":0.85,"y":1},"o":{"x":0.565,"y":0.108},"t":81,"s":[68.425,-66.7,0],"to":[17.562,-2.009,0],"ti":[-2.898,-109.235,0]},{"t":110,"s":[96.661,310.307,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":71,"s":[7,7,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":99,"s":[22,22,100]},{"t":111,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":74,"op":108,"st":71,"bm":0},{"ddd":0,"ind":19,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.404,"y":0.702},"o":{"x":0.125,"y":0.16},"t":73,"s":[46.772,16.267,0],"to":[-33.457,-55.77,0],"ti":[103.874,-12.223,0]},{"i":{"x":0.778,"y":1},"o":{"x":0.6,"y":0.342},"t":95,"s":[-172.095,-127.807,0],"to":[-137.203,16.145,0],"ti":[0,0,0]},{"t":131,"s":[-365.485,195.643,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.597,0.597,0.407],"y":[1,1,1]},"o":{"x":[0.05,0.05,0.05],"y":[0,0,0]},"t":73,"s":[-13,13,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":116,"s":[-26,26,100]},{"t":129,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":76,"op":123,"st":69,"bm":0},{"ddd":0,"ind":20,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.3,"y":0.886},"o":{"x":0.15,"y":0},"t":84,"s":[28.885,17.743,0],"to":[0,0,0],"ti":[7.443,1.184,0]},{"i":{"x":0.85,"y":1},"o":{"x":0.64,"y":0.076},"t":101,"s":[19.957,-150.575,0],"to":[-7.443,-1.184,0],"ti":[-0.07,-88.869,0]},{"t":136,"s":[11.456,323.592,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":84,"s":[6.7,6.7,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":126,"s":[24,24,100]},{"t":137,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":85,"op":134,"st":26,"bm":0},{"ddd":0,"ind":21,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.408,"y":0.879},"o":{"x":0.11,"y":0},"t":92,"s":[32.209,11.097,0],"to":[-11.213,-42.899,0],"ti":[51.566,-5.031,0]},{"i":{"x":0.781,"y":1},"o":{"x":0.7,"y":0.124},"t":111,"s":[-84.342,-168.527,0],"to":[-82.41,8.04,0],"ti":[0,0,0]},{"t":150,"s":[-230.85,330.743,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.61,0.61,0.61],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":92,"s":[6.806,6.806,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":138,"s":[20,20,100]},{"t":150,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":94,"op":144,"st":92,"bm":0},{"ddd":0,"ind":22,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.32,"y":0.911},"o":{"x":0.05,"y":0},"t":110,"s":[51.425,5.3,0],"to":[0,-1.667,0],"ti":[-8.263,0.945,0]},{"i":{"x":0.85,"y":1},"o":{"x":0.565,"y":0.044},"t":124,"s":[68.425,-106.7,0],"to":[17.562,-2.009,0],"ti":[-2.898,-109.235,0]},{"t":155,"s":[92.661,315.307,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":110,"s":[7,7,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":144,"s":[21,21,100]},{"t":156,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":111,"op":153,"st":110,"bm":0},{"ddd":0,"ind":23,"ty":0,"nm":"BODY 1 Precomp","parent":1,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.283,"y":0.57},"o":{"x":0.11,"y":0},"t":114,"s":[56.059,29.159,0],"to":[-53.493,-80.535,0],"ti":[60.825,-1.431,0]},{"i":{"x":0.806,"y":1},"o":{"x":0.6,"y":0.332},"t":132,"s":[-116.735,-142.127,0],"to":[-113.9,2.68,0],"ti":[0,0,0]},{"t":165,"s":[-333.655,288.733,0]}]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":114,"s":[10,10,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":152,"s":[26,26,100]},{"t":166,"s":[0,0,100]}]}},"ao":0,"w":512,"h":512,"ip":116,"op":161,"st":115,"bm":0}]} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 191c4968ce..4787fd71a9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -80,6 +80,7 @@ import PremiumUI import ImageTransparency import StickerPackPreviewUI import TextNodeWithEntities +import EntityKeyboard #if DEBUG import os.signpost @@ -1137,7 +1138,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G preconditionFailure() } - return ChatEntityKeyboardInputNode.emojiInputData( + return EmojiPagerContentComponent.emojiInputData( context: strongSelf.context, animationCache: animationCache, animationRenderer: animationRenderer, @@ -1673,26 +1674,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: chosenReaction) { - for reaction in availableReactions.reactions { - guard let centerAnimation = reaction.centerAnimation else { - continue - } - guard let aroundAnimation = reaction.aroundAnimation else { - continue - } - - if reaction.value == chosenReaction { - let standaloneReactionAnimation = StandaloneReactionAnimation() - - strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) - - strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) - standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - standaloneReactionAnimation.animateReactionSelection( - context: strongSelf.context, - theme: strongSelf.presentationData.theme, - animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache, - reaction: ReactionItem( + var reactionItem: ReactionItem? + + switch chosenReaction { + case .builtin: + for reaction in availableReactions.reactions { + guard let centerAnimation = reaction.centerAnimation else { + continue + } + guard let aroundAnimation = reaction.aroundAnimation else { + continue + } + if reaction.value == chosenReaction { + reactionItem = ReactionItem( reaction: ReactionItem.Reaction(rawValue: reaction.value), appearAnimation: reaction.appearAnimation, stillAnimation: reaction.selectAnimation, @@ -1701,26 +1695,53 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G applicationAnimation: aroundAnimation, largeApplicationAnimation: reaction.effectAnimation, isCustom: false - ), - avatarPeers: [], - playHaptic: false, - isLarge: false, - targetView: targetView, - addStandaloneReactionAnimation: { standaloneReactionAnimation in - guard let strongSelf = self else { - return - } - strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) - standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) - }, - completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - } - ) - - break + ) + break + } } + case let .custom(fileId): + if let itemFile = item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile { + reactionItem = ReactionItem( + reaction: ReactionItem.Reaction(rawValue: chosenReaction), + appearAnimation: itemFile, + stillAnimation: itemFile, + listAnimation: itemFile, + largeListAnimation: itemFile, + applicationAnimation: nil, + largeApplicationAnimation: nil, + isCustom: true + ) + } + } + + if let reactionItem = reactionItem { + let standaloneReactionAnimation = StandaloneReactionAnimation() + + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + standaloneReactionAnimation.animateReactionSelection( + context: strongSelf.context, + theme: strongSelf.presentationData.theme, + animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache, + reaction: reactionItem, + avatarPeers: [], + playHaptic: false, + isLarge: false, + targetView: targetView, + addStandaloneReactionAnimation: { standaloneReactionAnimation in + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + }, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) } } }) @@ -4444,7 +4465,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] { peers[associatedPeer.id] = associatedPeer } - renderedPeer = RenderedPeer(peerId: peer.id, peers: peers) + renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: peerView.media) } var isNotAccessible: Bool = false @@ -4741,7 +4762,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] { peers[associatedPeer.id] = associatedPeer } - renderedPeer = RenderedPeer(peerId: peer.id, peers: peers) + renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: peerView.media) } var isNotAccessible: Bool = false diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index f12a1e79b1..adc7c416cd 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -26,27 +26,6 @@ import TelegramPresentationData import TelegramNotices import StickerPeekUI -private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = { - guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else { - return [] - } - guard let string = try? String(contentsOf: URL(fileURLWithPath: path)) else { - return [] - } - - var result: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = [] - - let orderedSegments = EmojiPagerContentComponent.StaticEmojiSegment.allCases - - let segments = string.components(separatedBy: "\n\n") - for i in 0 ..< min(segments.count, orderedSegments.count) { - let list = segments[i].components(separatedBy: " ") - result.append((orderedSegments[i], list)) - } - - return result -}() - final class EntityKeyboardGifContent: Equatable { let hasRecentGifs: Bool let component: GifPagerContentComponent @@ -104,382 +83,6 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return hasPremium } - static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, isStatusSelection: Bool, isReactionSelection: Bool, reactionItems: [AvailableReactions.Reaction], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?) -> Signal { - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) - let isPremiumDisabled = premiumConfiguration.isPremiumDisabled - - let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings - - var orderedItemListCollectionIds: [Int32] = [] - - orderedItemListCollectionIds.append(Namespaces.OrderedItemList.LocalRecentEmoji) - - if isStatusSelection { - orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) - orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji) - } - - let emojiItems: Signal = combineLatest( - context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), - ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true), - context.account.viewTracker.featuredEmojiPacks() - ) - |> map { view, hasPremium, featuredEmojiPacks -> EmojiPagerContentComponent in - struct ItemGroup { - var supergroupId: AnyHashable - var id: AnyHashable - var title: String? - var subtitle: String? - var isPremiumLocked: Bool - var isFeatured: Bool - var isExpandable: Bool - var headerItem: EntityKeyboardAnimationData? - var items: [EmojiPagerContentComponent.Item] - } - var itemGroups: [ItemGroup] = [] - var itemGroupIndexById: [AnyHashable: Int] = [:] - - var recentEmoji: OrderedItemListView? - var featuredStatusEmoji: OrderedItemListView? - var recentStatusEmoji: OrderedItemListView? - for orderedView in view.orderedItemListsViews { - if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji { - recentEmoji = orderedView - } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedStatusEmoji { - featuredStatusEmoji = orderedView - } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStatusEmoji { - recentStatusEmoji = orderedView - } - } - - if isStatusSelection { - let resultItem = EmojiPagerContentComponent.Item( - animationData: nil, - content: .icon(.premiumStar), - itemFile: nil, - subgroupId: nil - ) - - let groupId = "recent" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) - } - - var existingIds = Set() - if let recentStatusEmoji = recentStatusEmoji { - for item in recentStatusEmoji.items { - guard let item = item.contents.get(RecentMediaItem.self) else { - continue - } - - let file = item.media - if existingIds.contains(file.fileId) { - continue - } - existingIds.insert(file.fileId) - - let resultItem: EmojiPagerContentComponent.Item - - let animationData = EntityKeyboardAnimationData(file: file) - resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: file, - subgroupId: nil - ) - - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } - } - } - if let featuredStatusEmoji = featuredStatusEmoji { - for item in featuredStatusEmoji.items { - guard let item = item.contents.get(RecentMediaItem.self) else { - continue - } - - let file = item.media - if existingIds.contains(file.fileId) { - continue - } - existingIds.insert(file.fileId) - - let resultItem: EmojiPagerContentComponent.Item - - let animationData = EntityKeyboardAnimationData(file: file) - resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: file, - subgroupId: nil - ) - - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } - } - } - } else if isReactionSelection { - for reactionItem in reactionItems { - let animationFile = reactionItem.selectAnimation - let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: animationFile, - subgroupId: nil - ) - - let groupId = "recent" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) - } - } - } - - if let recentEmoji = recentEmoji, !isReactionSelection, !isStatusSelection { - for item in recentEmoji.items { - guard let item = item.contents.get(RecentEmojiItem.self) else { - continue - } - - if case let .file(file) = item.content, isPremiumDisabled, file.isPremiumEmoji { - continue - } - - if !areCustomEmojiEnabled, case .file = item.content { - continue - } - - let resultItem: EmojiPagerContentComponent.Item - switch item.content { - case let .file(file): - let animationData = EntityKeyboardAnimationData(file: file) - resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: file, - subgroupId: nil - ) - case let .text(text): - resultItem = EmojiPagerContentComponent.Item( - animationData: nil, - content: .staticEmoji(text), - itemFile: nil, - subgroupId: nil - ) - } - - let groupId = "recent" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Emoji_FrequentlyUsed, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) - } - } - } - - if areUnicodeEmojiEnabled { - for (subgroupId, list) in staticEmojiMapping { - let groupId: AnyHashable = "static" - for emojiString in list { - let resultItem = EmojiPagerContentComponent.Item( - animationData: nil, - content: .staticEmoji(emojiString), - itemFile: nil, - subgroupId: subgroupId.rawValue - ) - - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleEmoji, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, headerItem: nil, items: [resultItem])) - } - } - } - } - - var installedCollectionIds = Set() - for (id, _, _) in view.collectionInfos { - installedCollectionIds.insert(id) - } - - if areCustomEmojiEnabled { - for entry in view.entries { - guard let item = entry.item as? StickerPackItem else { - continue - } - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil - ) - - let supergroupId = entry.index.collectionId - let groupId: AnyHashable = supergroupId - let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium - if isPremiumLocked && isPremiumDisabled { - continue - } - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - - var title = "" - var headerItem: EntityKeyboardAnimationData? - inner: for (id, info, _) in view.collectionInfos { - if id == entry.index.collectionId, let info = info as? StickerPackCollectionInfo { - title = info.title - - if let thumbnail = info.thumbnail { - let type: EntityKeyboardAnimationData.ItemType - if item.file.isAnimatedSticker { - type = .lottie - } else if item.file.isVideoEmoji || item.file.isVideoSticker { - type = .video - } else { - type = .still - } - - headerItem = EntityKeyboardAnimationData( - id: .stickerPackThumbnail(info.id), - type: type, - resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), - dimensions: thumbnail.dimensions.cgSize, - immediateThumbnailData: info.immediateThumbnailData, - isReaction: false - ) - } - - break inner - } - } - itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: false, isExpandable: false, headerItem: headerItem, items: [resultItem])) - } - } - - if !isStandalone { - for featuredEmojiPack in featuredEmojiPacks { - if installedCollectionIds.contains(featuredEmojiPack.info.id) { - continue - } - - for item in featuredEmojiPack.topItems { - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil - ) - - let supergroupId = featuredEmojiPack.info.id - let groupId: AnyHashable = supergroupId - let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium - if isPremiumLocked && isPremiumDisabled { - continue - } - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - - var headerItem: EntityKeyboardAnimationData? - if let thumbnailFileId = featuredEmojiPack.info.thumbnailFileId, let file = featuredEmojiPack.topItems.first(where: { $0.file.fileId.id == thumbnailFileId }) { - headerItem = EntityKeyboardAnimationData(file: file.file) - } else if let thumbnail = featuredEmojiPack.info.thumbnail { - let info = featuredEmojiPack.info - let type: EntityKeyboardAnimationData.ItemType - if item.file.isAnimatedSticker { - type = .lottie - } else if item.file.isVideoEmoji || item.file.isVideoSticker { - type = .video - } else { - type = .still - } - - headerItem = EntityKeyboardAnimationData( - id: .stickerPackThumbnail(info.id), - type: type, - resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), - dimensions: thumbnail.dimensions.cgSize, - immediateThumbnailData: info.immediateThumbnailData, - isReaction: false - ) - } - - itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: true, isExpandable: true, headerItem: headerItem, items: [resultItem])) - } - } - } - } - } - - return EmojiPagerContentComponent( - id: "emoji", - context: context, - avatarPeer: nil, - animationCache: animationCache, - animationRenderer: animationRenderer, - inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), - itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in - var hasClear = false - if group.id == AnyHashable("recent") { - hasClear = true - } - - var headerItem = group.headerItem - - if let groupId = group.id.base as? ItemCollectionId { - outer: for (id, info, _) in view.collectionInfos { - if id == groupId, let info = info as? StickerPackCollectionInfo { - if let thumbnailFileId = info.thumbnailFileId { - for item in group.items { - if let itemFile = item.itemFile, itemFile.fileId.id == thumbnailFileId { - headerItem = EntityKeyboardAnimationData(file: itemFile) - break outer - } - } - } - } - } - } - - return EmojiPagerContentComponent.ItemGroup( - supergroupId: group.supergroupId, - groupId: group.id, - title: group.title, - subtitle: group.subtitle, - actionButtonTitle: nil, - isFeatured: group.isFeatured, - isPremiumLocked: group.isPremiumLocked, - isEmbedded: false, - hasClear: hasClear, - isExpandable: group.isExpandable, - displayPremiumBadges: false, - headerItem: headerItem, - items: group.items - ) - }, - itemLayoutType: .compact, - warpContentsOnEdges: isReactionSelection || isStatusSelection - ) - } - return emojiItems - } - static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled @@ -494,7 +97,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationRenderer = MultiAnimationRendererImpl() //} - let emojiItems = emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId) + let emojiItems = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId) let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers] @@ -2415,7 +2018,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV let semaphore = DispatchSemaphore(value: 0) var emojiComponent: EmojiPagerContentComponent? - let _ = ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil).start(next: { value in + let _ = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil).start(next: { value in emojiComponent = value semaphore.signal() }) @@ -2430,7 +2033,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV gifs: nil, availableGifSearchEmojies: [] ), - updatedInputData: ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in + updatedInputData: EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, reactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in return ChatEntityKeyboardInputNode.InputData( emoji: emojiComponent, stickers: nil, diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index 69ef326a17..63b0de8680 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -15,12 +15,12 @@ import WallpaperBackgroundNode func canViewMessageReactionList(message: Message) -> Bool { var found = false + var canViewList = false for attribute in message.attributes { if let attribute = attribute as? ReactionsMessageAttribute { - if !attribute.canViewList { - return false - } + canViewList = attribute.canViewList found = true + break } } @@ -33,9 +33,11 @@ func canViewMessageReactionList(message: Message) -> Bool { if case .broadcast = channel.info { return false } else { - return true + return canViewList } } else if let _ = peer as? TelegramGroup { + return canViewList + } else if let _ = peer as? TelegramUser { return true } else { return false diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 159cea8ccd..109d0de7ce 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -12,6 +12,8 @@ import ChatListUI import AccountContext import ContextUI import ChatListSearchItemHeader +import AnimationCache +import MultiAnimationRenderer private enum ChatListSearchEntryStableId: Hashable { case messageId(MessageId) @@ -135,6 +137,8 @@ private func chatListSearchContainerPreparedTransition(from fromEntries: [ChatLi class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let location: SearchMessagesLocation private let searchQuery: String private var searchResult: SearchMessagesResult @@ -169,6 +173,11 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe self.presentationData = presentationData self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)) + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + self.listNode = ListView() self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor self.listNode.accessibilityPageScrolledString = { row, count in @@ -198,7 +207,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe return entries } - let interaction = ChatListNodeInteraction(context: context, activateSearch: { + let interaction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { }, peerSelected: { _, _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in diff --git a/submodules/TelegramUI/Sources/ChatTitleView.swift b/submodules/TelegramUI/Sources/ChatTitleView.swift index 5bf864094d..60ab1e6a8e 100644 --- a/submodules/TelegramUI/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Sources/ChatTitleView.swift @@ -63,7 +63,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { private let animationRenderer: MultiAnimationRenderer private let contentContainer: ASDisplayNode - let titleNode: ImmediateAnimatedCountLabelNode + let titleContainerView: PortalSourceView + let titleTextNode: ImmediateAnimatedCountLabelNode let titleLeftIconNode: ASImageNode let titleRightIconNode: ASImageNode let titleCredibilityIconView: ComponentHostView @@ -252,8 +253,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { var updated = false - if self.titleNode.segments != segments { - self.titleNode.segments = segments + if self.titleTextNode.segments != segments { + self.titleTextNode.segments = segments updated = true } @@ -525,7 +526,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } var accessibilityText = "" - for segment in self.titleNode.segments { + for segment in self.titleTextNode.segments { switch segment { case let .number(_, string): accessibilityText.append(string.string) @@ -563,7 +564,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.contentContainer = ASDisplayNode() - self.titleNode = ImmediateAnimatedCountLabelNode() + self.titleContainerView = PortalSourceView() + self.titleTextNode = ImmediateAnimatedCountLabelNode() self.titleLeftIconNode = ASImageNode() self.titleLeftIconNode.isLayerBacked = true @@ -587,7 +589,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.accessibilityTraits = .header self.addSubnode(self.contentContainer) - self.contentContainer.addSubnode(self.titleNode) + self.titleContainerView.addSubnode(self.titleTextNode) + self.contentContainer.view.addSubview(self.titleContainerView) self.contentContainer.addSubnode(self.activityNode) self.addSubnode(self.button) @@ -599,17 +602,17 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.button.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.titleTextNode.layer.removeAnimation(forKey: "opacity") strongSelf.activityNode.layer.removeAnimation(forKey: "opacity") strongSelf.titleCredibilityIconView.layer.removeAnimation(forKey: "opacity") - strongSelf.titleNode.alpha = 0.4 + strongSelf.titleTextNode.alpha = 0.4 strongSelf.activityNode.alpha = 0.4 strongSelf.titleCredibilityIconView.alpha = 0.4 } else { - strongSelf.titleNode.alpha = 1.0 + strongSelf.titleTextNode.alpha = 1.0 strongSelf.activityNode.alpha = 1.0 strongSelf.titleCredibilityIconView.alpha = 1.0 - strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.titleTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.activityNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } @@ -656,7 +659,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let image = self.titleLeftIconNode.image { if self.titleLeftIconNode.supernode == nil { - self.titleNode.addSubnode(self.titleLeftIconNode) + self.titleTextNode.addSubnode(self.titleLeftIconNode) } leftIconWidth = image.size.width + 6.0 } else if self.titleLeftIconNode.supernode != nil { @@ -694,7 +697,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { ) if self.titleCredibilityIcon != .none { - self.titleNode.view.addSubview(self.titleCredibilityIconView) + self.titleTextNode.view.addSubview(self.titleCredibilityIconView) credibilityIconWidth = titleCredibilitySize.width + 3.0 } else { if self.titleCredibilityIconView.superview != nil { @@ -704,7 +707,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let image = self.titleRightIconNode.image { if self.titleRightIconNode.supernode == nil { - self.titleNode.addSubnode(self.titleRightIconNode) + self.titleTextNode.addSubnode(self.titleRightIconNode) } rightIconWidth = image.size.width + 3.0 } else if self.titleRightIconNode.supernode != nil { @@ -714,7 +717,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { let titleSideInset: CGFloat = 3.0 var titleFrame: CGRect if size.height > 40.0 { - var titleSize = self.titleNode.updateLayout(size: CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height), animated: transition.isAnimated) + var titleSize = self.titleTextNode.updateLayout(size: CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height), animated: transition.isAnimated) titleSize.width += credibilityIconWidth let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .center) let titleInfoSpacing: CGFloat = 0.0 @@ -724,7 +727,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if titleFrame.size.width < size.width { titleFrame.origin.x = -clearBounds.minX + floor((size.width - titleFrame.width) / 2.0) } - transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) + transition.updateFrameAdditive(view: self.titleContainerView, frame: titleFrame) + transition.updateFrameAdditive(node: self.titleTextNode, frame: CGRect(origin: CGPoint(), size: titleFrame.size)) } else { let combinedHeight = titleSize.height + activitySize.height + titleInfoSpacing @@ -733,7 +737,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { titleFrame.origin.x = -clearBounds.minX + floor((size.width - titleFrame.width) / 2.0) } titleFrame.origin.x = max(titleFrame.origin.x, clearBounds.minX + leftIconWidth) - transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) + transition.updateFrameAdditive(view: self.titleContainerView, frame: titleFrame) + transition.updateFrameAdditive(node: self.titleTextNode, frame: CGRect(origin: CGPoint(), size: titleFrame.size)) var activityFrame = CGRect(origin: CGPoint(x: floor((clearBounds.width - activitySize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: activitySize) if activitySize.width < size.width { @@ -752,14 +757,17 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width + 3.0 + UIScreenPixel, y: 6.0), size: image.size) } } else { - let titleSize = self.titleNode.updateLayout(size: CGSize(width: floor(clearBounds.width / 2.0 - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0), height: size.height), animated: transition.isAnimated) + let titleSize = self.titleTextNode.updateLayout(size: CGSize(width: floor(clearBounds.width / 2.0 - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0), height: size.height), animated: transition.isAnimated) let activitySize = self.activityNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0), height: size.height), alignment: .center) let titleInfoSpacing: CGFloat = 8.0 let combinedWidth = titleSize.width + leftIconWidth + credibilityIconWidth + rightIconWidth + activitySize.width + titleInfoSpacing titleFrame = CGRect(origin: CGPoint(x: leftIconWidth + floor((clearBounds.width - combinedWidth) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) - transition.updateFrameAdditiveToCenter(node: self.titleNode, frame: titleFrame) + + transition.updateFrameAdditiveToCenter(view: self.titleContainerView, frame: titleFrame) + transition.updateFrameAdditiveToCenter(node: self.titleTextNode, frame: CGRect(origin: CGPoint(), size: titleFrame.size)) + self.activityNode.frame = CGRect(origin: CGPoint(x: floor((clearBounds.width - combinedWidth) / 2.0 + titleSize.width + leftIconWidth + credibilityIconWidth + rightIconWidth + titleInfoSpacing), y: floor((size.height - activitySize.height) / 2.0)), size: activitySize) if let image = self.titleLeftIconNode.image { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 3c8bc2b6c4..21480c6728 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -9,6 +9,8 @@ import MergeLists import AccountContext import ContactListUI import ChatListUI +import AnimationCache +import MultiAnimationRenderer private struct SearchResultEntry: Identifiable { let index: Int @@ -66,12 +68,20 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { private var presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter], limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?) { self.navigationBar = navigationBar self.context = context self.presentationData = presentationData + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + var placeholder: String var includeChatList = false switch mode { @@ -88,7 +98,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { if case let .chatSelection(_, selectedChats, additionalCategories, chatListFilters) = mode { placeholder = self.presentationData.strings.ChatListFilter_AddChatsTitle - let chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters), theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) + let chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters), theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true) if let limit = limit { chatListNode.selectionLimit = limit chatListNode.reachedSelectionLimit = reachedSelectionLimit diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 1fcdb849c7..f125064568 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -2201,12 +2201,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { let phoneGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePhoneLongPress(_:))) self.subtitleNodeRawContainer.view.addGestureRecognizer(phoneGestureRecognizer) - - /*let premiumGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleStarTap(_:))) - self.titleCredibilityIconNode.view.addGestureRecognizer(premiumGestureRecognizer) - - let expandedPremiumGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleStarTap(_:))) - self.titleExpandedCredibilityIconNode.view.addGestureRecognizer(expandedPremiumGestureRecognizer)*/ } @objc private func handleUsernameLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { @@ -2341,76 +2335,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { if themeUpdated || self.currentCredibilityIcon != credibilityIcon { self.currentCredibilityIcon = credibilityIcon - let image: UIImage? - var expandedImage: UIImage? - - if case .fake = credibilityIcon { - image = PresentationResourcesChatList.fakeIcon(presentationData.theme, strings: presentationData.strings, type: .regular) - } else if case .scam = credibilityIcon { - image = PresentationResourcesChatList.scamIcon(presentationData.theme, strings: presentationData.strings, type: .regular) - } else if case .verified = credibilityIcon { - if let backgroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconForeground") { - image = generateImage(backgroundImage.size, contextGenerator: { size, context in - if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.saveGState() - context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) - - context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.restoreGState() - - context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) - context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - expandedImage = generateImage(backgroundImage.size, contextGenerator: { size, context in - if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.saveGState() - context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) - context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.restoreGState() - - context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) - context.setBlendMode(.clear) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - } else { - image = nil - } - } else if case .premium = credibilityIcon { - if let sourceImage = UIImage(bundleImageName: "Peer Info/PremiumIcon") { - image = generateImage(sourceImage.size, contextGenerator: { size, context in - if let cgImage = sourceImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) - - context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - expandedImage = generateImage(sourceImage.size, contextGenerator: { size, context in - if let cgImage = sourceImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) - context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - } else { - image = nil - } - } else { - image = nil - } - - let _ = image - let _ = expandedImage - var currentEmojiStatus: PeerEmojiStatus? let emojiRegularStatusContent: EmojiStatusComponent.Content let emojiExpandedStatusContent: EmojiStatusComponent.Content @@ -2436,8 +2360,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { emojiExpandedStatusContent = .emojiStatus(status: emojiStatus, size: CGSize(width: 32.0, height: 32.0), placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.15)) } + let animateStatusIcon = !self.titleCredibilityIconView.bounds.isEmpty + let iconSize = self.titleCredibilityIconView.update( - transition: Transition(transition), + transition: animateStatusIcon ? Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) : .immediate, component: AnyComponent(EmojiStatusComponent( context: self.context, animationCache: self.animationCache, @@ -2497,7 +2423,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { containerSize: CGSize(width: 34.0, height: 34.0) ) let expandedIconSize = self.titleExpandedCredibilityIconView.update( - transition: Transition(transition), + transition: animateStatusIcon ? Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) : .immediate, component: AnyComponent(EmojiStatusComponent( context: self.context, animationCache: self.animationCache, @@ -2522,17 +2448,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.credibilityIconSize = iconSize self.titleExpandedCredibilityIconSize = expandedIconSize - - /*if let image = image { - self.credibilityIconSize = image.size - self.titleExpandedCredibilityIconSize = (expandedImage ?? image).size - } else { - self.credibilityIconSize = nil - self.titleExpandedCredibilityIconSize = nil - }*/ - - //self.titleCredibilityIconNode.image = image - //self.titleExpandedCredibilityIconNode.image = expandedImage ?? image } self.regularContentNode.alpha = state.isEditing ? 0.0 : 1.0 @@ -2755,7 +2670,13 @@ final class PeerInfoHeaderNode: ASDisplayNode { var titleHorizontalOffset: CGFloat = 0.0 if let credibilityIconSize = self.credibilityIconSize, let titleExpandedCredibilityIconSize = self.titleExpandedCredibilityIconSize { titleHorizontalOffset = -(credibilityIconSize.width + 4.0) / 2.0 - transition.updateFrame(view: self.titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0, y: floor((titleSize.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize)) + + var collapsedTransitionOffset: CGFloat = 0.0 + if let navigationTransition = self.navigationTransition { + collapsedTransitionOffset = -10.0 * navigationTransition.fraction + } + + transition.updateFrame(view: self.titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0 + collapsedTransitionOffset, y: floor((titleSize.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize)) transition.updateFrame(view: self.titleExpandedCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleExpandedSize.width + 4.0, y: floor((titleExpandedSize.height - titleExpandedCredibilityIconSize.height) / 2.0) + 1.0), size: titleExpandedCredibilityIconSize)) } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 0b4cf3e348..55cf19dc48 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -72,6 +72,7 @@ import InstantPageCache import EmojiStatusSelectionComponent import AnimationCache import MultiAnimationRenderer +import EntityKeyboard protocol PeerInfoScreenItem: AnyObject { var id: AnyHashable { get } @@ -3095,7 +3096,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate strongSelf.controller?.present(EmojiStatusSelectionController( context: strongSelf.context, sourceView: sourceView, - emojiContent: ChatEntityKeyboardInputNode.emojiInputData( + emojiContent: EmojiPagerContentComponent.emojiInputData( context: strongSelf.context, animationCache: animationCache, animationRenderer: animationRenderer, @@ -8487,7 +8488,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig private var previousBackButtonBadge: ASDisplayNode? private var currentBackButton: ASDisplayNode? - private var previousTitleNode: (ASDisplayNode, ASDisplayNode)? + private var previousTitleNode: (ASDisplayNode, PortalView)? private var previousStatusNode: (ASDisplayNode, ASDisplayNode)? private var didSetup: Bool = false @@ -8538,11 +8539,12 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig self.currentBackButton = currentBackButton self.addSubnode(currentBackButton) } - if let previousTitleView = bottomNavigationBar.titleView as? ChatTitleView { - let previousTitleNode = previousTitleView.titleNode.makeCopy() + if let previousTitleView = bottomNavigationBar.titleView as? ChatTitleView, let previousTitleNode = PortalView(matchPosition: false) { + previousTitleNode.view.frame = previousTitleView.titleContainerView.frame + previousTitleView.titleContainerView.addPortal(view: previousTitleNode) let previousTitleContainerNode = ASDisplayNode() - previousTitleContainerNode.addSubnode(previousTitleNode) - previousTitleNode.frame = previousTitleNode.frame.offsetBy(dx: -previousTitleNode.frame.width / 2.0, dy: -previousTitleNode.frame.height / 2.0) + previousTitleContainerNode.view.addSubview(previousTitleNode.view) + previousTitleNode.view.frame = previousTitleNode.view.frame.offsetBy(dx: -previousTitleNode.view.frame.width / 2.0, dy: -previousTitleNode.view.frame.height / 2.0) self.previousTitleNode = (previousTitleContainerNode, previousTitleNode) self.addSubnode(previousTitleContainerNode) @@ -8596,12 +8598,16 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } if let previousTitleView = bottomNavigationBar.titleView as? ChatTitleView, let _ = (bottomNavigationBar.rightButtonNode.singleCustomNode as? ChatAvatarNavigationNode)?.avatarNode, let (previousTitleContainerNode, previousTitleNode) = self.previousTitleNode, let (previousStatusContainerNode, previousStatusNode) = self.previousStatusNode { - let previousTitleFrame = previousTitleView.titleNode.view.convert(previousTitleView.titleNode.bounds, to: bottomNavigationBar.view) + let previousTitleFrame = previousTitleView.titleContainerView.convert(previousTitleView.titleContainerView.bounds, to: bottomNavigationBar.view) let previousStatusFrame = previousTitleView.activityNode.view.convert(previousTitleView.activityNode.bounds, to: bottomNavigationBar.view) self.headerNode.navigationTransition = PeerInfoHeaderNavigationTransition(sourceNavigationBar: bottomNavigationBar, sourceTitleView: previousTitleView, sourceTitleFrame: previousTitleFrame, sourceSubtitleFrame: previousStatusFrame, fraction: fraction) var topHeight = topNavigationBar.backgroundNode.bounds.height + if let iconView = previousTitleView.titleCredibilityIconView.componentView { + transition.updateFrame(view: iconView, frame: iconView.bounds.offsetBy(dx: (1.0 - fraction) * 8.0, dy: 0.0)) + } + if let (layout, _) = self.screenNode.validLayout { let sectionInset: CGFloat if layout.size.width >= 375.0 { @@ -8614,11 +8620,11 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, notificationSettings: self.screenNode.data?.notificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, transition: transition, additive: false) } - let titleScale = (fraction * previousTitleNode.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.bounds.height + let titleScale = (fraction * previousTitleNode.view.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.view.bounds.height let subtitleScale = max(0.01, min(10.0, (fraction * previousStatusNode.bounds.height + (1.0 - fraction) * self.headerNode.subtitleNodeRawContainer.bounds.height) / previousStatusNode.bounds.height)) transition.updateFrame(node: previousTitleContainerNode, frame: CGRect(origin: self.headerNode.titleNodeRawContainer.frame.center, size: CGSize())) - transition.updateFrame(node: previousTitleNode, frame: CGRect(origin: CGPoint(x: -previousTitleFrame.width / 2.0, y: -previousTitleFrame.height / 2.0), size: previousTitleFrame.size)) + transition.updateFrame(view: previousTitleNode.view, frame: CGRect(origin: CGPoint(x: -previousTitleFrame.width / 2.0, y: -previousTitleFrame.height / 2.0), size: previousTitleFrame.size)) transition.updateFrame(node: previousStatusContainerNode, frame: CGRect(origin: self.headerNode.subtitleNodeRawContainer.frame.center, size: CGSize())) transition.updateFrame(node: previousStatusNode, frame: CGRect(origin: CGPoint(x: -previousStatusFrame.size.width / 2.0, y: -previousStatusFrame.size.height / 2.0), size: previousStatusFrame.size)) @@ -8626,7 +8632,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig transition.updateSublayerTransformScale(node: previousStatusContainerNode, scale: subtitleScale) transition.updateAlpha(node: self.headerNode.titleNode, alpha: (1.0 - fraction)) - transition.updateAlpha(node: previousTitleNode, alpha: fraction) + transition.updateAlpha(layer: previousTitleNode.view.layer, alpha: fraction) transition.updateAlpha(node: self.headerNode.subtitleNode, alpha: (1.0 - fraction)) transition.updateAlpha(node: previousStatusNode, alpha: fraction) diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index 798e6d568a..071a961d68 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -16,6 +16,8 @@ import AttachmentTextInputPanelNode import ChatPresentationInterfaceState import ChatSendMessageActionUI import ChatTextLinkEditUI +import AnimationCache +import MultiAnimationRenderer final class PeerSelectionControllerNode: ASDisplayNode { private let context: AccountContext @@ -72,6 +74,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { } private var presentationDataPromise = Promise() + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + private var readyValue = Promise() var ready: Signal { return self.readyValue.get() @@ -93,6 +98,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.presentationData = presentationData + self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) } @@ -120,7 +130,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { chatListCategories.append(ChatListNodeAdditionalCategory(id: 0, icon: PresentationResourcesItemList.createGroupIcon(self.presentationData.theme), title: self.presentationData.strings.PeerSelection_ImportIntoNewGroup, appearance: .action)) } - self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) + self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true) super.init() @@ -584,6 +594,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { presentationData: self.presentationData, contentNode: ChatListSearchContainerNode( context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, updatedPresentationData: self.updatedPresentationData, filter: self.filter, groupId: EngineChatList.Group(.root), diff --git a/submodules/lottie-ios/Sources/Private/CoreAnimation/CoreAnimationLayer.swift b/submodules/lottie-ios/Sources/Private/CoreAnimation/CoreAnimationLayer.swift index 2507d457d8..d36f608299 100644 --- a/submodules/lottie-ios/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +++ b/submodules/lottie-ios/Sources/Private/CoreAnimation/CoreAnimationLayer.swift @@ -424,6 +424,11 @@ extension CoreAnimationLayer: RootAnimationLayer { LottieLogger.shared.assertionFailure("`AnimationKeypath`s are currently unsupported") return nil } + + func allLayers(for keypath: AnimationKeypath) -> [CALayer] { + LottieLogger.shared.assertionFailure("`AnimationKeypath`s are currently unsupported") + return [] + } func animatorNodes(for _: AnimationKeypath) -> [AnimatorNode]? { LottieLogger.shared.assertionFailure("`AnimatorNode`s are not used in this rendering implementation") diff --git a/submodules/lottie-ios/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift b/submodules/lottie-ios/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift index ffc0d1eab4..3b3518f169 100644 --- a/submodules/lottie-ios/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +++ b/submodules/lottie-ios/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift @@ -255,6 +255,14 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer { } return nil } + + func allLayers(for keypath: AnimationKeypath) -> [CALayer] { + var result: [CALayer] = [] + for layer in animationLayers { + result.append(contentsOf: layer.allLayers(for: keypath)) + } + return result + } func animatorNodes(for keypath: AnimationKeypath) -> [AnimatorNode]? { var results = [AnimatorNode]() diff --git a/submodules/lottie-ios/Sources/Private/RootAnimationLayer.swift b/submodules/lottie-ios/Sources/Private/RootAnimationLayer.swift index ea586f66c4..be86aa4446 100644 --- a/submodules/lottie-ios/Sources/Private/RootAnimationLayer.swift +++ b/submodules/lottie-ios/Sources/Private/RootAnimationLayer.swift @@ -39,6 +39,7 @@ protocol RootAnimationLayer: CALayer { func getOriginalValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? func layer(for keypath: AnimationKeypath) -> CALayer? + func allLayers(for keypath: AnimationKeypath) -> [CALayer] func animatorNodes(for keypath: AnimationKeypath) -> [AnimatorNode]? } diff --git a/submodules/lottie-ios/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift b/submodules/lottie-ios/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift index 7e31179780..94cd15df26 100644 --- a/submodules/lottie-ios/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +++ b/submodules/lottie-ios/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift @@ -114,6 +114,29 @@ extension KeypathSearchable { } return nil } + + func allLayers(for keyPath: AnimationKeypath) -> [CALayer] { + if keyPath.nextKeypath == nil, let layerKey = keyPath.currentKey, layerKey.equalsKeypath(keypathName) { + /// We found our layer! + + if let keypathLayer = self.keypathLayer { + return [keypathLayer] + } else { + return [] + } + } + guard let nextKeypath = keyPath.popKey(keypathName) else { + /// Nope. Stop Search + return [] + } + + /// Now check child keypaths. + var foundSublayers: [CALayer] = [] + for child in childKeypaths { + foundSublayers.append(contentsOf: child.allLayers(for: nextKeypath)) + } + return foundSublayers + } func logKeypaths(for keyPath: AnimationKeypath?) { let newKeypath: AnimationKeypath diff --git a/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift b/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift index f2b1b96186..3408de4a2b 100644 --- a/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift +++ b/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift @@ -659,6 +659,10 @@ final public class AnimationView: AnimationViewBase { sublayer.addSublayer(subViewLayer) } } + + public func allLayers(forKeypath keypath: AnimationKeypath) -> [CALayer] { + return animationLayer?.allLayers(for: keypath) ?? [] + } /// Converts a CGRect from the AnimationView's coordinate space into the /// coordinate space of the layer found at Keypath.