mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Reaction and status improvements
This commit is contained in:
parent
5b12540c44
commit
5d969ed2af
55 changed files with 1475 additions and 748 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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]()
|
||||
|
|
|
|||
|
|
@ -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]?
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue