Reaction and status improvements

This commit is contained in:
Ali 2022-08-23 02:58:31 +03:00
parent 5b12540c44
commit 5d969ed2af
55 changed files with 1475 additions and 748 deletions

View file

@ -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",

View file

@ -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<NetworkStatusTitle.Status?, NoError> = 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

View file

@ -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

View file

@ -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<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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()

View file

@ -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<PresentationData, NoError>)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group?, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) {
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group?, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, 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 })

View file

@ -7,6 +7,8 @@ import TelegramPresentationData
import TelegramCore
import AccountContext
import ContextUI
import AnimationCache
import MultiAnimationRenderer
protocol ChatListSearchPaneNode: ASDisplayNode {
var isReady: Signal<Bool, NoError> { get }
@ -102,6 +104,8 @@ private final class ChatListSearchPendingPane {
init(
context: AccountContext,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
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<PresentationData, NoError>)?
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<PresentationData, NoError>)? = nil, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) {
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, 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,

View file

@ -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<Empty>?
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<Empty>
if let current = self.titleCredibilityIconView {
titleCredibilityIconView = current
} else {
titleCredibilityIconTransition = .immediate
titleCredibilityIconView = ComponentHostView<Empty>()
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<Empty>
if let current = self.titleCredibilityIconView {
titleCredibilityIconView = current
} else {
titleCredibilityIconTransition = .immediate
titleCredibilityIconView = ComponentHostView<Empty>()
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)
}

View file

@ -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<Bool>()
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()
}

View file

@ -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() {

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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",

View file

@ -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

View file

@ -335,6 +335,60 @@ public struct ChatListAdditionalItemEntry: Equatable {
}
}
func renderAssociatedMediaForPeers(postbox: PostboxImpl, peers: SimpleDictionary<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
}
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
}

View file

@ -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 {

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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<PeerId, Peer>()
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)))
}
}
}

View file

@ -3,15 +3,18 @@ import Foundation
public final class RenderedPeer: Equatable {
public let peerId: PeerId
public let peers: SimpleDictionary<PeerId, Peer>
public let associatedMedia: [MediaId: Media]
public init(peerId: PeerId, peers: SimpleDictionary<PeerId, Peer>) {
public init(peerId: PeerId, peers: SimpleDictionary<PeerId, Peer>, 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
}

View file

@ -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<EmojiPagerContentComponent, NoError>)?
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<EmojiPagerContentComponent, NoError>)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void) {
public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?, 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<Int>()
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
}

View file

@ -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()

View file

@ -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<Bool>
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()

View file

@ -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<Bool>
@ -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()

View file

@ -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)
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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?

View file

@ -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

View file

@ -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)
}
}

View file

@ -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) {

View file

@ -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))
}
}

View file

@ -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? {

View file

@ -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()
}
}
}

View file

@ -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",

View file

@ -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(

View file

@ -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<EnvironmentType>, 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<Bool, NoError> {
let hasPremium: Signal<Bool, NoError>
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<EmojiPagerContentComponent, NoError> {
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<EmojiPagerContentComponent, NoError> = 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<MediaId>()
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<ItemCollectionId>()
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
}
}

File diff suppressed because one or more lines are too long

View file

@ -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

View file

@ -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<EmojiPagerContentComponent, NoError> {
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<EmojiPagerContentComponent, NoError> = 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<MediaId>()
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<ItemCollectionId>()
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<InputData, NoError> {
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,

View file

@ -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

View file

@ -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

View file

@ -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<Empty>
@ -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 {

View file

@ -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

View file

@ -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))
}

View file

@ -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)

View file

@ -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<PresentationData>()
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private var readyValue = Promise<Bool>()
var ready: Signal<Bool, NoError> {
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),

View file

@ -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")

View file

@ -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]()

View file

@ -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]?
}

View file

@ -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

View file

@ -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.