mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 14:45:21 +00:00
Support updated API
This commit is contained in:
@@ -44,724 +44,6 @@ public enum ChatListContainerNodeFilter: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private final class ShimmerEffectNode: ASDisplayNode {
|
||||
private var currentBackgroundColor: UIColor?
|
||||
private var currentForegroundColor: UIColor?
|
||||
private let imageNodeContainer: ASDisplayNode
|
||||
private let imageNode: ASImageNode
|
||||
|
||||
private var absoluteLocation: (CGRect, CGSize)?
|
||||
private var isCurrentlyInHierarchy = false
|
||||
private var shouldBeAnimating = false
|
||||
|
||||
override init() {
|
||||
self.imageNodeContainer = ASDisplayNode()
|
||||
self.imageNodeContainer.isLayerBacked = true
|
||||
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.isLayerBacked = true
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.imageNode.displayWithoutProcessing = true
|
||||
self.imageNode.contentMode = .scaleToFill
|
||||
|
||||
super.init()
|
||||
|
||||
self.isLayerBacked = true
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.imageNodeContainer.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.imageNodeContainer)
|
||||
}
|
||||
|
||||
override func didEnterHierarchy() {
|
||||
super.didEnterHierarchy()
|
||||
|
||||
self.isCurrentlyInHierarchy = true
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
override func didExitHierarchy() {
|
||||
super.didExitHierarchy()
|
||||
|
||||
self.isCurrentlyInHierarchy = false
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
|
||||
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) {
|
||||
return
|
||||
}
|
||||
self.currentBackgroundColor = backgroundColor
|
||||
self.currentForegroundColor = foregroundColor
|
||||
|
||||
self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.clip(to: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
|
||||
let peakColor = foregroundColor.cgColor
|
||||
|
||||
var locations: [CGFloat] = [0.0, 0.5, 1.0]
|
||||
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}
|
||||
|
||||
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
|
||||
return
|
||||
}
|
||||
let sizeUpdated = self.absoluteLocation?.1 != containerSize
|
||||
let frameUpdated = self.absoluteLocation?.0 != rect
|
||||
self.absoluteLocation = (rect, containerSize)
|
||||
|
||||
if sizeUpdated {
|
||||
if self.shouldBeAnimating {
|
||||
self.imageNode.layer.removeAnimation(forKey: "shimmer")
|
||||
self.addImageAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
if frameUpdated {
|
||||
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
|
||||
}
|
||||
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
private func updateAnimation() {
|
||||
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
|
||||
if shouldBeAnimating != self.shouldBeAnimating {
|
||||
self.shouldBeAnimating = shouldBeAnimating
|
||||
if shouldBeAnimating {
|
||||
self.addImageAnimation()
|
||||
} else {
|
||||
self.imageNode.layer.removeAnimation(forKey: "shimmer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addImageAnimation() {
|
||||
guard let containerSize = self.absoluteLocation?.1 else {
|
||||
return
|
||||
}
|
||||
let gradientHeight: CGFloat = 250.0
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
|
||||
let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
|
||||
animation.repeatCount = Float.infinity
|
||||
animation.beginTime = 1.0
|
||||
self.imageNode.layer.add(animation, forKey: "shimmer")
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatListShimmerNode: ASDisplayNode {
|
||||
private let backgroundColorNode: ASDisplayNode
|
||||
private let effectNode: ShimmerEffectNode
|
||||
private let maskNode: ASImageNode
|
||||
private var currentParams: (size: CGSize, presentationData: PresentationData)?
|
||||
|
||||
override init() {
|
||||
self.backgroundColorNode = ASDisplayNode()
|
||||
self.effectNode = ShimmerEffectNode()
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubnode(self.backgroundColorNode)
|
||||
self.addSubnode(self.effectNode)
|
||||
self.addSubnode(self.maskNode)
|
||||
}
|
||||
|
||||
func update(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, size: CGSize, isInlineMode: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
|
||||
if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData {
|
||||
self.currentParams = (size, presentationData)
|
||||
|
||||
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
|
||||
|
||||
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, usernames: [], storiesHidden: nil))
|
||||
let timestamp1: Int32 = 100000
|
||||
let peers: [EnginePeer.Id: EnginePeer] = [:]
|
||||
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 }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in })
|
||||
interaction.isInlineMode = isInlineMode
|
||||
|
||||
let items = (0 ..< 2).map { _ -> ChatListItem in
|
||||
let message = EngineMessage(
|
||||
stableId: 0,
|
||||
stableVersion: 0,
|
||||
id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0),
|
||||
globallyUniqueId: nil,
|
||||
groupingKey: nil,
|
||||
groupInfo: nil,
|
||||
threadId: nil,
|
||||
timestamp: timestamp1,
|
||||
flags: [],
|
||||
tags: [],
|
||||
globalTags: [],
|
||||
localTags: [],
|
||||
forwardInfo: nil,
|
||||
author: peer1,
|
||||
text: "Text",
|
||||
attributes: [],
|
||||
media: [],
|
||||
peers: peers,
|
||||
associatedMessages: [:],
|
||||
associatedMessageIds: [],
|
||||
associatedMedia: [:],
|
||||
associatedThreadInfo: nil,
|
||||
associatedStories: [:]
|
||||
)
|
||||
let readState = EnginePeerReadCounters()
|
||||
|
||||
return ChatListItem(presentationData: chatListPresentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1))), content: .peer(ChatListItemContent.PeerData(
|
||||
messages: [message],
|
||||
peer: EngineRenderedPeer(peer: peer1),
|
||||
threadInfo: nil,
|
||||
combinedReadState: readState,
|
||||
isRemovedFromTotalUnreadCount: false,
|
||||
presence: nil,
|
||||
hasUnseenMentions: false,
|
||||
hasUnseenReactions: false,
|
||||
draftState: nil,
|
||||
inputActivities: nil,
|
||||
promoInfo: nil,
|
||||
ignoreUnreadBadge: false,
|
||||
displayAsMessage: false,
|
||||
hasFailedMessages: false,
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
storyState: nil
|
||||
)), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
}
|
||||
|
||||
var itemNodes: [ChatListItemNode] = []
|
||||
for i in 0 ..< items.count {
|
||||
items[i].nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 100.0), synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: (i == items.count - 1) ? nil : items[i + 1], completion: { node, apply in
|
||||
if let itemNode = node as? ChatListItemNode {
|
||||
itemNodes.append(itemNode)
|
||||
}
|
||||
apply().1(ListViewItemApply(isOnScreen: true))
|
||||
})
|
||||
}
|
||||
|
||||
self.backgroundColorNode.backgroundColor = presentationData.theme.list.mediaPlaceholderColor
|
||||
|
||||
self.maskNode.image = generateImage(size, rotatedContext: { size, context in
|
||||
context.setFillColor(presentationData.theme.chatList.backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
var currentY: CGFloat = 0.0
|
||||
let fakeLabelPlaceholderHeight: CGFloat = 8.0
|
||||
|
||||
func fillLabelPlaceholderRect(origin: CGPoint, width: CGFloat) {
|
||||
let startPoint = origin
|
||||
let diameter = fakeLabelPlaceholderHeight
|
||||
context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter)))
|
||||
context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter)))
|
||||
}
|
||||
|
||||
while currentY < size.height {
|
||||
let sampleIndex = 0
|
||||
let itemHeight: CGFloat = itemNodes[sampleIndex].contentSize.height
|
||||
|
||||
context.setBlendMode(.copy)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
|
||||
if !isInlineMode {
|
||||
if !itemNodes[sampleIndex].avatarNode.isHidden {
|
||||
context.fillEllipse(in: itemNodes[sampleIndex].avatarNode.view.convert(itemNodes[sampleIndex].avatarNode.bounds, to: itemNodes[sampleIndex].view).offsetBy(dx: 0.0, dy: currentY))
|
||||
}
|
||||
}
|
||||
|
||||
let titleFrame = itemNodes[sampleIndex].titleNode.frame.offsetBy(dx: 0.0, dy: currentY)
|
||||
if isInlineMode {
|
||||
fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX + 22.0, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0 - 22.0)
|
||||
} else {
|
||||
fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0)
|
||||
}
|
||||
|
||||
let textFrame = itemNodes[sampleIndex].textNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY)
|
||||
|
||||
if isInlineMode {
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: textFrame.minX, y: titleFrame.minY + 2.0), size: CGSize(width: 16.0, height: 16.0)))
|
||||
}
|
||||
|
||||
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 60.0)
|
||||
|
||||
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 120.0)
|
||||
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX + 120.0 + 10.0, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 60.0)
|
||||
|
||||
let dateFrame = itemNodes[sampleIndex].dateNode.frame.offsetBy(dx: 0.0, dy: currentY)
|
||||
fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY), width: 30.0)
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor)
|
||||
context.fill(itemNodes[sampleIndex].separatorNode.frame.offsetBy(dx: 0.0, dy: currentY))
|
||||
|
||||
currentY += itemHeight
|
||||
}
|
||||
})
|
||||
|
||||
self.effectNode.update(backgroundColor: presentationData.theme.list.mediaPlaceholderColor, foregroundColor: presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4))
|
||||
self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size)
|
||||
}
|
||||
transition.updateFrame(node: self.backgroundColorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatListContainerItemNode: ASDisplayNode {
|
||||
private final class TopPanelItem {
|
||||
let view = ComponentView<Empty>()
|
||||
var size: CGSize?
|
||||
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private weak var controller: ChatListControllerImpl?
|
||||
private let location: ChatListControllerLocation
|
||||
private let animationCache: AnimationCache
|
||||
private let animationRenderer: MultiAnimationRenderer
|
||||
private var presentationData: PresentationData
|
||||
private let becameEmpty: (ChatListFilter?) -> Void
|
||||
private let emptyAction: (ChatListFilter?) -> Void
|
||||
private let secondaryEmptyAction: () -> Void
|
||||
private let openArchiveSettings: () -> Void
|
||||
private let isInlineMode: Bool
|
||||
|
||||
private var floatingHeaderOffset: CGFloat?
|
||||
|
||||
private(set) var emptyNode: ChatListEmptyNode?
|
||||
var emptyShimmerEffectNode: ChatListShimmerNode?
|
||||
private var shimmerNodeOffset: CGFloat = 0.0
|
||||
let listNode: ChatListNode
|
||||
|
||||
private var topPanel: TopPanelItem?
|
||||
|
||||
private var pollFilterUpdatesDisposable: Disposable?
|
||||
private var chatFilterUpdatesDisposable: Disposable?
|
||||
private var peerDataDisposable: Disposable?
|
||||
|
||||
private var chatFolderUpdates: ChatFolderUpdates?
|
||||
|
||||
private var canReportPeer: Bool = false
|
||||
|
||||
private(set) var validLayout: (size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat)?
|
||||
private var scrollingOffset: (navigationHeight: CGFloat, offset: CGFloat)?
|
||||
|
||||
init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void, autoSetReady: Bool) {
|
||||
self.context = context
|
||||
self.controller = controller
|
||||
self.location = location
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = animationRenderer
|
||||
self.presentationData = presentationData
|
||||
self.becameEmpty = becameEmpty
|
||||
self.emptyAction = emptyAction
|
||||
self.secondaryEmptyAction = secondaryEmptyAction
|
||||
self.openArchiveSettings = openArchiveSettings
|
||||
self.isInlineMode = isInlineMode
|
||||
|
||||
self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady)
|
||||
|
||||
if let controller, case .chatList(groupId: .root) = controller.location {
|
||||
self.listNode.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight + ChatListNavigationBar.storiesScrollHeight
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.listNode)
|
||||
|
||||
self.listNode.isEmptyUpdated = { [weak self] isEmptyState, _, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var needsShimmerNode = false
|
||||
var shimmerNodeOffset: CGFloat = 0.0
|
||||
|
||||
var needsEmptyNode = false
|
||||
var hasOnlyArchive = false
|
||||
var hasOnlyGeneralThread = false
|
||||
var isLoading = false
|
||||
|
||||
switch isEmptyState {
|
||||
case let .empty(isLoadingValue, hasArchiveInfo):
|
||||
if hasArchiveInfo {
|
||||
shimmerNodeOffset = 253.0
|
||||
}
|
||||
if isLoadingValue {
|
||||
needsShimmerNode = true
|
||||
needsEmptyNode = false
|
||||
isLoading = isLoadingValue
|
||||
} else {
|
||||
needsEmptyNode = true
|
||||
}
|
||||
if !isLoadingValue {
|
||||
strongSelf.becameEmpty(filter)
|
||||
}
|
||||
case let .notEmpty(_, onlyHasArchiveValue, onlyGeneralThreadValue):
|
||||
needsEmptyNode = onlyHasArchiveValue || onlyGeneralThreadValue
|
||||
hasOnlyArchive = onlyHasArchiveValue
|
||||
hasOnlyGeneralThread = onlyGeneralThreadValue
|
||||
}
|
||||
|
||||
if needsEmptyNode {
|
||||
if let currentNode = strongSelf.emptyNode {
|
||||
currentNode.updateIsLoading(isLoading)
|
||||
} else {
|
||||
let subject: ChatListEmptyNode.Subject
|
||||
if let filter = filter {
|
||||
var showEdit = true
|
||||
if case let .filter(_, _, _, data) = filter {
|
||||
if data.excludeRead && data.includePeers.peers.isEmpty && data.includePeers.pinnedPeers.isEmpty {
|
||||
showEdit = false
|
||||
}
|
||||
}
|
||||
subject = .filter(showEdit: showEdit)
|
||||
} else {
|
||||
if case .forum = location {
|
||||
subject = .forum(hasGeneral: hasOnlyGeneralThread)
|
||||
} else {
|
||||
if case .chatList(groupId: .archive) = location {
|
||||
subject = .archive
|
||||
} else {
|
||||
subject = .chats(hasArchive: hasOnlyArchive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let emptyNode = ChatListEmptyNode(context: context, subject: subject, isLoading: isLoading, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, action: {
|
||||
self?.emptyAction(filter)
|
||||
}, secondaryAction: {
|
||||
self?.secondaryEmptyAction()
|
||||
}, openArchiveSettings: {
|
||||
self?.openArchiveSettings()
|
||||
})
|
||||
strongSelf.emptyNode = emptyNode
|
||||
strongSelf.listNode.addSubnode(emptyNode)
|
||||
if let (size, insets, _, _, _, _, _) = strongSelf.validLayout {
|
||||
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
|
||||
emptyNode.frame = emptyNodeFrame
|
||||
emptyNode.updateLayout(size: size, insets: insets, transition: .immediate)
|
||||
|
||||
if let scrollingOffset = strongSelf.scrollingOffset {
|
||||
emptyNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: .immediate)
|
||||
}
|
||||
}
|
||||
emptyNode.alpha = 0.0
|
||||
transition.updateAlpha(node: emptyNode, alpha: 1.0)
|
||||
}
|
||||
} else if let emptyNode = strongSelf.emptyNode {
|
||||
strongSelf.emptyNode = nil
|
||||
transition.updateAlpha(node: emptyNode, alpha: 0.0, completion: { [weak emptyNode] _ in
|
||||
emptyNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if needsShimmerNode {
|
||||
strongSelf.shimmerNodeOffset = shimmerNodeOffset
|
||||
if strongSelf.emptyShimmerEffectNode == nil {
|
||||
let emptyShimmerEffectNode = ChatListShimmerNode()
|
||||
strongSelf.emptyShimmerEffectNode = emptyShimmerEffectNode
|
||||
strongSelf.insertSubnode(emptyShimmerEffectNode, belowSubnode: strongSelf.listNode)
|
||||
if let (size, insets, _, _, _, _, _) = strongSelf.validLayout, let offset = strongSelf.floatingHeaderOffset {
|
||||
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: .immediate)
|
||||
}
|
||||
}
|
||||
} else if let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode {
|
||||
strongSelf.emptyShimmerEffectNode = nil
|
||||
let emptyNodeTransition = transition.isAnimated ? transition : .animated(duration: 0.3, curve: .easeInOut)
|
||||
emptyNodeTransition.updateAlpha(node: emptyShimmerEffectNode, alpha: 0.0, completion: { [weak emptyShimmerEffectNode] _ in
|
||||
emptyShimmerEffectNode?.removeFromSupernode()
|
||||
})
|
||||
strongSelf.listNode.alpha = 0.0
|
||||
emptyNodeTransition.updateAlpha(node: strongSelf.listNode, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.floatingHeaderOffset = offset
|
||||
if let (size, insets, _, _, _, _, _) = strongSelf.validLayout, let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode {
|
||||
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: transition)
|
||||
}
|
||||
strongSelf.layoutAdditionalPanels(transition: transition)
|
||||
}
|
||||
|
||||
if let filter, case let .filter(id, _, _, data) = filter, data.isShared {
|
||||
self.pollFilterUpdatesDisposable = self.context.engine.peers.pollChatFolderUpdates(folderId: id).start()
|
||||
self.chatFilterUpdatesDisposable = (self.context.engine.peers.subscribedChatFolderUpdates(folderId: id)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var update = false
|
||||
if let result, result.availableChatsToJoin != 0 {
|
||||
if self.chatFolderUpdates?.availableChatsToJoin != result.availableChatsToJoin {
|
||||
update = true
|
||||
}
|
||||
self.chatFolderUpdates = result
|
||||
} else {
|
||||
if self.chatFolderUpdates != nil {
|
||||
self.chatFolderUpdates = nil
|
||||
update = true
|
||||
}
|
||||
}
|
||||
if update {
|
||||
if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
|
||||
self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if case let .forum(peerId) = location {
|
||||
self.peerDataDisposable = (context.engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Peer.StatusSettings(id: peerId)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] statusSettings in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var canReportPeer = false
|
||||
if let statusSettings, statusSettings.flags.contains(.canReport) {
|
||||
canReportPeer = true
|
||||
}
|
||||
if self.canReportPeer != canReportPeer {
|
||||
self.canReportPeer = canReportPeer
|
||||
if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
|
||||
self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.pollFilterUpdatesDisposable?.dispose()
|
||||
self.chatFilterUpdatesDisposable?.dispose()
|
||||
self.peerDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func layoutEmptyShimmerEffectNode(node: ChatListShimmerNode, size: CGSize, insets: UIEdgeInsets, verticalOffset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
node.update(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, size: size, isInlineMode: self.isInlineMode, presentationData: self.presentationData, transition: .immediate)
|
||||
transition.updateFrameAdditive(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: size))
|
||||
}
|
||||
|
||||
private func layoutAdditionalPanels(transition: ContainedViewLayoutTransition) {
|
||||
guard let (size, insets, visualNavigationHeight, _, _, _, _) = self.validLayout, let offset = self.floatingHeaderOffset else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = size
|
||||
let _ = insets
|
||||
|
||||
if let topPanel = self.topPanel, let topPanelSize = topPanel.size {
|
||||
let minY: CGFloat = visualNavigationHeight - 44.0 + topPanelSize.height
|
||||
|
||||
if let topPanelView = topPanel.view.view {
|
||||
var animateIn = false
|
||||
var topPanelTransition = transition
|
||||
if topPanelView.bounds.isEmpty {
|
||||
topPanelTransition = .immediate
|
||||
animateIn = true
|
||||
}
|
||||
topPanelTransition.updateFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: max(minY, offset - topPanelSize.height)), size: topPanelSize))
|
||||
if animateIn {
|
||||
transition.animatePositionAdditive(layer: topPanelView.layer, offset: CGPoint(x: 0.0, y: -topPanelView.bounds.height))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
|
||||
self.listNode.accessibilityPageScrolledString = { row, count in
|
||||
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
|
||||
}
|
||||
|
||||
self.listNode.updateThemeAndStrings(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
|
||||
|
||||
self.emptyNode?.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset)
|
||||
|
||||
var listInsets = insets
|
||||
var additionalTopInset: CGFloat = 0.0
|
||||
|
||||
if let chatFolderUpdates = self.chatFolderUpdates {
|
||||
let topPanel: TopPanelItem
|
||||
var topPanelTransition = Transition(transition)
|
||||
if let current = self.topPanel {
|
||||
topPanel = current
|
||||
} else {
|
||||
topPanelTransition = .immediate
|
||||
topPanel = TopPanelItem()
|
||||
self.topPanel = topPanel
|
||||
}
|
||||
|
||||
let title: String = self.presentationData.strings.ChatList_PanelNewChatsAvailable(Int32(chatFolderUpdates.availableChatsToJoin))
|
||||
|
||||
let topPanelHeight: CGFloat = 44.0
|
||||
|
||||
let _ = topPanel.view.update(
|
||||
transition: topPanelTransition,
|
||||
component: AnyComponent(ActionPanelComponent(
|
||||
theme: self.presentationData.theme,
|
||||
title: title,
|
||||
color: .accent,
|
||||
action: { [weak self] in
|
||||
guard let self, let chatFolderUpdates = self.chatFolderUpdates else {
|
||||
return
|
||||
}
|
||||
|
||||
self.listNode.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(chatFolderUpdates), contents: chatFolderUpdates.chatFolderLinkContents))
|
||||
},
|
||||
dismissAction: { [weak self] in
|
||||
guard let self, let chatFolderUpdates = self.chatFolderUpdates else {
|
||||
return
|
||||
}
|
||||
let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: chatFolderUpdates.folderId).start()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: topPanelHeight)
|
||||
)
|
||||
if let topPanelView = topPanel.view.view {
|
||||
if topPanelView.superview == nil {
|
||||
self.view.addSubview(topPanelView)
|
||||
}
|
||||
}
|
||||
|
||||
topPanel.size = CGSize(width: size.width, height: topPanelHeight)
|
||||
listInsets.top += topPanelHeight
|
||||
additionalTopInset += topPanelHeight
|
||||
} else if self.canReportPeer {
|
||||
let topPanel: TopPanelItem
|
||||
var topPanelTransition = Transition(transition)
|
||||
if let current = self.topPanel {
|
||||
topPanel = current
|
||||
} else {
|
||||
topPanelTransition = .immediate
|
||||
topPanel = TopPanelItem()
|
||||
self.topPanel = topPanel
|
||||
}
|
||||
|
||||
let title: String = self.presentationData.strings.Conversation_ReportSpamAndLeave
|
||||
|
||||
let topPanelHeight: CGFloat = 44.0
|
||||
|
||||
let _ = topPanel.view.update(
|
||||
transition: topPanelTransition,
|
||||
component: AnyComponent(ActionPanelComponent(
|
||||
theme: self.presentationData.theme,
|
||||
title: title,
|
||||
color: .destructive,
|
||||
action: { [weak self] in
|
||||
guard let self, case let .forum(peerId) = self.location else {
|
||||
return
|
||||
}
|
||||
|
||||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: self.presentationData.strings.Conversation_ReportSpamGroupConfirmation),
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ReportSpamAndLeave, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
if let self {
|
||||
self.controller?.setInlineChatList(location: nil)
|
||||
let _ = self.context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: true).start()
|
||||
}
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
self.listNode.present?(actionSheet)
|
||||
},
|
||||
dismissAction: { [weak self] in
|
||||
guard let self, case let .forum(peerId) = self.location else {
|
||||
return
|
||||
}
|
||||
let _ = self.context.engine.peers.dismissPeerStatusOptions(peerId: peerId).start()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: topPanelHeight)
|
||||
)
|
||||
if let topPanelView = topPanel.view.view {
|
||||
if topPanelView.superview == nil {
|
||||
self.view.addSubview(topPanelView)
|
||||
}
|
||||
}
|
||||
|
||||
topPanel.size = CGSize(width: size.width, height: topPanelHeight)
|
||||
listInsets.top += topPanelHeight
|
||||
additionalTopInset += topPanelHeight
|
||||
} else {
|
||||
if let topPanel = self.topPanel {
|
||||
self.topPanel = nil
|
||||
if let topPanelView = topPanel.view.view {
|
||||
transition.updatePosition(layer: topPanelView.layer, position: CGPoint(x: topPanelView.layer.position.x, y: topPanelView.layer.position.y - topPanelView.layer.bounds.height), completion: { [weak topPanelView] _ in
|
||||
topPanelView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: listInsets, duration: duration, curve: curve)
|
||||
|
||||
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: visualNavigationHeight + additionalTopInset, originalTopInset: originalNavigationHeight + additionalTopInset, storiesInset: storiesInset, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction)
|
||||
|
||||
if let emptyNode = self.emptyNode {
|
||||
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
|
||||
transition.updateFrame(node: emptyNode, frame: emptyNodeFrame)
|
||||
emptyNode.updateLayout(size: emptyNodeFrame.size, insets: listInsets, transition: transition)
|
||||
|
||||
if let scrollingOffset = self.scrollingOffset {
|
||||
emptyNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
self.layoutAdditionalPanels(transition: transition)
|
||||
}
|
||||
|
||||
func updateScrollingOffset(navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.scrollingOffset = (navigationHeight, offset)
|
||||
|
||||
if let emptyNode = self.emptyNode {
|
||||
emptyNode.updateScrollingOffset(navigationHeight: navigationHeight, offset: offset, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
private let context: AccountContext
|
||||
private weak var controller: ChatListControllerImpl?
|
||||
|
||||
Reference in New Issue
Block a user