mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
455 lines
24 KiB
Swift
455 lines
24 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import ComponentFlow
|
|
import AccountContext
|
|
import TelegramPresentationData
|
|
import SwiftSignalKit
|
|
import AnimationCache
|
|
import MultiAnimationRenderer
|
|
import TelegramCore
|
|
import Postbox
|
|
import ChatListHeaderComponent
|
|
import ActionPanelComponent
|
|
import ChatFolderLinkPreviewScreen
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|