2024-06-15 20:34:07 +04:00

650 lines
31 KiB
Swift

import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import AccountContext
import ContextUI
import TelegramStringFormatting
import ShimmerEffect
import ComponentFlow
import TelegramNotices
import TelegramUIPreferences
import AppBundle
import PeerInfoPaneNode
import ChatListUI
import DeleteChatPeerActionSheetItem
import UndoUI
private final class SearchNavigationContentNode: ASDisplayNode, PeerInfoPanelNodeNavigationContentNode {
private struct Params: Equatable {
var width: CGFloat
var defaultHeight: CGFloat
var insets: UIEdgeInsets
init(width: CGFloat, defaultHeight: CGFloat, insets: UIEdgeInsets) {
self.width = width
self.defaultHeight = defaultHeight
self.insets = insets
}
}
weak var chatController: ChatController?
let contentNode: NavigationBarContentNode
var panelNode: ChatControllerCustomNavigationPanelNode?
private var appliedPanelNode: ChatControllerCustomNavigationPanelNode?
private var params: Params?
init(chatController: ChatController, contentNode: NavigationBarContentNode) {
self.chatController = chatController
self.contentNode = contentNode
super.init()
self.addSubnode(self.contentNode)
}
func update(transition: ContainedViewLayoutTransition) {
if let params = self.params {
let _ = self.update(width: params.width, defaultHeight: params.defaultHeight, insets: params.insets, transition: transition)
}
}
func update(width: CGFloat, defaultHeight: CGFloat, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) -> CGFloat {
self.params = Params(width: width, defaultHeight: defaultHeight, insets: insets)
let size = CGSize(width: width, height: defaultHeight)
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 10.0), size: size))
self.contentNode.updateLayout(size: size, leftInset: insets.left, rightInset: insets.right, transition: transition)
var contentHeight: CGFloat = size.height + 10.0
if self.appliedPanelNode !== self.panelNode {
if let previous = self.appliedPanelNode {
transition.updateAlpha(node: previous, alpha: 0.0, completion: { [weak previous] _ in
previous?.removeFromSupernode()
})
}
self.appliedPanelNode = self.panelNode
if let panelNode = self.panelNode, let chatController = self.chatController {
self.addSubnode(panelNode)
let panelLayout = panelNode.updateLayout(width: width, leftInset: insets.left, rightInset: insets.right, transition: .immediate, chatController: chatController)
let panelHeight = panelLayout.backgroundHeight
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: panelHeight))
panelNode.frame = panelFrame
panelNode.alpha = 0.0
transition.updateAlpha(node: panelNode, alpha: 1.0)
contentHeight += panelHeight - 1.0
}
} else if let panelNode = self.panelNode, let chatController = self.chatController {
let panelLayout = panelNode.updateLayout(width: width, leftInset: insets.left, rightInset: insets.right, transition: transition, chatController: chatController)
let panelHeight = panelLayout.backgroundHeight
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: panelHeight))
transition.updateFrame(node: panelNode, frame: panelFrame)
contentHeight += panelHeight - 1.0
}
return contentHeight
}
}
public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
private let context: AccountContext
private let navigationController: () -> NavigationController?
public weak var parentController: ViewController?
private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)?
private let ready = Promise<Bool>()
private var didSetReady: Bool = false
public var isReady: Signal<Bool, NoError> {
return self.ready.get()
}
private let statusPromise = Promise<PeerInfoStatusData?>(nil)
public var status: Signal<PeerInfoStatusData?, NoError> {
self.statusPromise.get()
}
public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
public var tabBarOffset: CGFloat {
return 0.0
}
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let chatListNode: ChatListNode
private var emptyShimmerEffectNode: ChatListShimmerNode?
private var shimmerNodeOffset: CGFloat = 0.0
private var floatingHeaderOffset: CGFloat?
private let coveringView: UIView
private var chatController: ChatController?
private var removeChatWhenNotSearching: Bool = false
private var searchNavigationContentNode: SearchNavigationContentNode?
public var navigationContentNode: PeerInfoPanelNodeNavigationContentNode? {
return self.searchNavigationContentNode
}
public var externalDataUpdated: ((ContainedViewLayoutTransition) -> Void)?
public init(context: AccountContext, navigationController: @escaping () -> NavigationController?) {
self.context = context
self.navigationController = navigationController
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
let strings = presentationData.strings
self.coveringView = UIView()
self.chatListNode = ChatListNode(
context: self.context,
location: .savedMessagesChats,
chatListFilter: nil,
previewing: false,
fillPreloadItems: false,
mode: .chatList(appendContacts: false),
isPeerEnabled: nil,
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.context.animationCache,
animationRenderer: self.context.animationRenderer,
disableAnimations: false,
isInlineMode: false,
autoSetReady: false,
isMainTab: nil
)
self.chatListNode.synchronousDrawingWhenNotAnimated = true
super.init()
self.clipsToBounds = true
self.addSubnode(self.chatListNode)
self.view.addSubview(self.coveringView)
self.presentationDataDisposable = (self.context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
guard let self else {
return
}
self.presentationData = presentationData
})
self.ready.set(self.chatListNode.ready)
self.statusPromise.set(self.context.engine.messages.savedMessagesPeersStats()
|> map { count in
if let count {
return PeerInfoStatusData(text: strings.Notifications_Exceptions(Int32(count)), isActivity: false, key: .savedMessagesChats)
} else {
return PeerInfoStatusData(text: strings.Channel_NotificationLoading.lowercased(), isActivity: false, key: .savedMessagesChats)
}
})
self.chatListNode.peerSelected = { [weak self] peer, _, _, _, _ in
guard let self, let navigationController = self.navigationController() else {
return
}
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
context: self.context,
chatLocation: .replyThread(ChatReplyThreadMessage(
peerId: self.context.account.peerId,
threadId: peer.id.toInt64(),
channelMessageId: nil,
isChannelPost: false,
isForumPost: false,
maxMessage: nil,
maxReadIncomingMessageId: nil,
maxReadOutgoingMessageId: nil,
unreadCount: 0,
initialFilledHoles: IndexSet(),
initialAnchor: .automatic,
isNotAvailable: false
)),
subject: nil,
keepStack: .always
))
self.chatListNode.clearHighlightAnimated(true)
}
self.chatListNode.isEmptyUpdated = { [weak self] isEmptyState, _, transition in
guard let self else {
return
}
var needsShimmerNode = false
let shimmerNodeOffset: CGFloat = 0.0
switch isEmptyState {
case let .empty(isLoadingValue, _):
if isLoadingValue {
needsShimmerNode = true
}
case .notEmpty:
break
}
if needsShimmerNode {
self.shimmerNodeOffset = shimmerNodeOffset
if self.emptyShimmerEffectNode == nil {
let emptyShimmerEffectNode = ChatListShimmerNode()
self.emptyShimmerEffectNode = emptyShimmerEffectNode
self.insertSubnode(emptyShimmerEffectNode, belowSubnode: self.chatListNode)
if let currentParams = self.currentParams, let offset = self.floatingHeaderOffset {
self.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: currentParams.size, insets: UIEdgeInsets(top: currentParams.topInset, left: currentParams.sideInset, bottom: currentParams.bottomInset, right: currentParams.sideInset), verticalOffset: offset + self.shimmerNodeOffset, transition: .immediate)
}
}
} else if let emptyShimmerEffectNode = self.emptyShimmerEffectNode {
self.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()
})
self.chatListNode.alpha = 0.0
emptyNodeTransition.updateAlpha(node: self.chatListNode, alpha: 1.0)
}
}
self.chatListNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
guard let self else {
return
}
self.floatingHeaderOffset = offset
if let currentParams = self.currentParams, let emptyShimmerEffectNode = self.emptyShimmerEffectNode {
self.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: currentParams.size, insets: UIEdgeInsets(top: currentParams.topInset, left: currentParams.sideInset, bottom: currentParams.bottomInset, right: currentParams.sideInset), verticalOffset: offset + self.shimmerNodeOffset, transition: transition)
}
}
self.chatListNode.push = { [weak self] c in
guard let self else {
return
}
self.parentController?.push(c)
}
self.chatListNode.present = { [weak self] c in
guard let self else {
return
}
self.parentController?.present(c, in: .window(.root))
}
self.chatListNode.deletePeerChat = { [weak self] peerId, _ in
guard let self else {
return
}
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
self.view.window?.endEditing(true)
let actionSheet = ActionSheetController(presentationData: self.presentationData)
var items: [ActionSheetItem] = []
items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: peer, chatPeer: peer, action: .deleteSavedPeer, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, balancedLayout: true))
items.append(ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self else {
return
}
self.chatListNode.updateState({ state in
var state = state
state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peer.id, threadId: nil))
return state
})
self.parentController?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitActionAndReplacementAnimation()
}
return true
})
if self.chatListNode.entryPeerIds.count == 0 || self.chatListNode.entryPeerIds == [peer.id] {
let _ = context.engine.messages.clearHistoryInteractively(peerId: self.context.account.peerId, threadId: peer.id.toInt64(), type: .forLocalPeer).startStandalone(completed: {
})
context.engine.peers.updateSavedMessagesViewAsTopics(value: false)
self.parentController?.dismiss()
return
}
let context = self.context
let undoController = UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: self.presentationData.strings.SavedMessages_SubChatDeleted, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in
if value == .commit {
let _ = context.engine.messages.clearHistoryInteractively(peerId: context.account.peerId, threadId: peer.id.toInt64(), type: .forLocalPeer).startStandalone(completed: {
guard let self else {
return
}
self.chatListNode.updateState({ state in
var state = state
state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peer.id, threadId: nil))
return state
})
})
return true
} else if value == .undo {
if let self {
self.chatListNode.updateState({ state in
var state = state
state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peer.id, threadId: nil))
return state
})
}
return true
}
return false
})
self.parentController?.present(undoController, in: .window(.root))
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
self.parentController?.present(actionSheet, in: .window(.root))
})
}
self.chatListNode.activateChatPreview = { [weak self] item, _, node, gesture, location in
guard let self, let parentController = self.parentController else {
gesture?.cancel()
return
}
if case let .peer(peerData) = item.content {
let threadId = peerData.peer.peerId.toInt64()
let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .replyThread(message: ChatReplyThreadMessage(
peerId: self.context.account.peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false
)), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil)
chatController.canReadHistory.set(false)
let source: ContextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: parentController.navigationController as? NavigationController))
let contextController = ContextController(presentationData: self.presentationData, source: source, items: savedMessagesPeerMenuItems(context: self.context, threadId: threadId, parentController: parentController, deletePeerChat: { [weak self] peerId in
guard let self else {
return
}
self.chatListNode.deletePeerChat?(peerId, false)
}) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
parentController.presentInGlobalOverlay(contextController)
}
}
}
deinit {
self.presentationDataDisposable?.dispose()
}
public func activateSearch() {
if self.chatController == nil {
let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: self.context.account.peerId), subject: nil, botStart: nil, mode: .standard(.embedded(invertDirection: false)), params: nil)
chatController.alwaysShowSearchResultsAsList = true
chatController.includeSavedPeersInSearchResults = true
self.chatController = chatController
chatController.navigation_setNavigationController(self.navigationController())
self.insertSubnode(chatController.displayNode, aboveSubnode: self.chatListNode)
chatController.displayNode.alpha = 0.0
chatController.displayNode.clipsToBounds = true
self.updateChatController(transition: .immediate)
let _ = (chatController.ready.get()
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak chatController] _ in
guard let self, let chatController, self.chatController === chatController else {
return
}
chatController.customDismissSearch = { [weak self] in
guard let self else {
return
}
if self.searchNavigationContentNode !== nil {
self.searchNavigationContentNode = nil
self.externalDataUpdated?(.animated(duration: 0.4, curve: .spring))
}
self.removeChatController()
}
chatController.stateUpdated = { [weak self] transition in
guard let self, let chatController = self.chatController else {
return
}
if let contentNode = chatController.customNavigationBarContentNode {
self.removeChatWhenNotSearching = true
chatController.displayNode.layer.allowsGroupOpacity = true
if transition.isAnimated {
ComponentTransition.easeInOut(duration: 0.2).setAlpha(layer: chatController.displayNode.layer, alpha: 1.0)
}
if self.searchNavigationContentNode?.contentNode !== contentNode {
self.searchNavigationContentNode = SearchNavigationContentNode(chatController: chatController, contentNode: contentNode)
self.searchNavigationContentNode?.panelNode = chatController.customNavigationPanelNode
self.externalDataUpdated?(transition)
} else if self.searchNavigationContentNode?.panelNode !== chatController.customNavigationPanelNode {
self.searchNavigationContentNode?.panelNode = chatController.customNavigationPanelNode
self.externalDataUpdated?(transition.isAnimated ? transition : .animated(duration: 0.4, curve: .spring))
} else {
self.searchNavigationContentNode?.update(transition: transition)
}
} else {
if self.searchNavigationContentNode !== nil {
self.searchNavigationContentNode = nil
self.externalDataUpdated?(transition)
}
if self.removeChatWhenNotSearching {
self.removeChatController()
}
}
}
chatController.activateSearch(domain: .everything, query: "")
})
}
}
private func removeChatController() {
if let chatController = self.chatController {
self.chatController = nil
let displayNode = chatController.displayNode
chatController.displayNode.layer.allowsGroupOpacity = true
chatController.displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak displayNode] _ in
displayNode?.removeFromSupernode()
})
}
}
public func ensureMessageIsVisible(id: MessageId) {
}
public func scrollToTop() -> Bool {
if let chatController = self.chatController {
let _ = chatController.performScrollToTop()
} else {
self.chatListNode.scrollToPosition(.top(adjustForTempInset: false))
}
return false
}
public func hitTestResultForScrolling() -> UIView? {
return nil
}
public func brieflyDisableTouchActions() {
}
public func findLoadedMessage(id: MessageId) -> Message? {
return nil
}
public func updateHiddenMedia() {
}
public func transferVelocity(_ velocity: CGFloat) {
}
public func cancelPreviewGestures() {
}
public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
public func addToTransitionSurface(view: UIView) {
}
override public func didLoad() {
super.didLoad()
}
override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
private func layoutEmptyShimmerEffectNode(node: ChatListShimmerNode, size: CGSize, insets: UIEdgeInsets, verticalOffset: CGFloat, transition: ContainedViewLayoutTransition) {
node.update(context: self.context, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, size: size, isInlineMode: false, presentationData: self.presentationData, transition: .immediate)
transition.updateFrameAdditive(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: size))
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.state != .failed, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer {
let _ = otherGestureRecognizer
return true
} else {
return false
}
}
public func updateSelectedMessages(animated: Bool) {
}
private func updateChatController(transition: ContainedViewLayoutTransition) {
guard let chatController = self.chatController else {
return
}
guard let currentParams = self.currentParams else {
return
}
let size = currentParams.size
let topInset = currentParams.topInset
let sideInset = currentParams.sideInset
let bottomInset = currentParams.bottomInset
let navigationHeight = currentParams.navigationHeight
let deviceMetrics = currentParams.deviceMetrics
let isScrollingLockedAtTop = currentParams.isScrollingLockedAtTop
let fullHeight = navigationHeight + size.height
let chatFrame = CGRect(origin: CGPoint(x: 0.0, y: -navigationHeight), size: CGSize(width: size.width, height: fullHeight))
if !chatController.displayNode.bounds.isEmpty {
if let contextController = chatController.visibleContextController as? ContextController {
let deltaY = chatFrame.minY - chatController.displayNode.frame.minY
contextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -deltaY * 0.0), transition: transition)
}
}
let combinedBottomInset = bottomInset
transition.updateFrame(node: chatController.displayNode, frame: chatFrame)
chatController.updateIsScrollingLockedAtTop(isScrollingLockedAtTop: isScrollingLockedAtTop)
chatController.containerLayoutUpdated(ContainerViewLayout(size: chatFrame.size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: deviceMetrics, intrinsicInsets: UIEdgeInsets(top: topInset + navigationHeight, left: sideInset, bottom: combinedBottomInset, right: sideInset), safeInsets: UIEdgeInsets(top: navigationHeight + topInset + 4.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition)
}
public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics: deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData)
self.coveringView.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
transition.updateFrame(view: self.coveringView, frame: CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: size.width, height: topInset + 1.0)))
let fullHeight = navigationHeight + size.height
let chatFrame = CGRect(origin: CGPoint(x: 0.0, y: -navigationHeight), size: CGSize(width: size.width, height: fullHeight))
let combinedBottomInset = bottomInset
transition.updateFrame(node: self.chatListNode, frame: chatFrame)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.chatListNode.updateLayout(
transition: transition,
updateSizeAndInsets: ListViewUpdateSizeAndInsets(
size: size,
insets: UIEdgeInsets(top: topInset + navigationHeight, left: sideInset, bottom: combinedBottomInset, right: sideInset),
duration: duration,
curve: curve
),
visibleTopInset: topInset + navigationHeight,
originalTopInset: topInset + navigationHeight,
storiesInset: 0.0,
inlineNavigationLocation: nil,
inlineNavigationTransitionFraction: 0.0
)
self.updateChatController(transition: transition)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
return result
}
}
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
let controller: ViewController
weak var sourceNode: ASDisplayNode?
let navigationController: NavigationController?
let passthroughTouches: Bool = true
init(controller: ViewController, sourceNode: ASDisplayNode?, navigationController: NavigationController?) {
self.controller = controller
self.sourceNode = sourceNode
self.navigationController = navigationController
}
func transitionInfo() -> ContextControllerTakeControllerInfo? {
let sourceNode = self.sourceNode
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
if let sourceNode = sourceNode {
return (sourceNode.view, sourceNode.bounds)
} else {
return nil
}
})
}
func animatedIn() {
}
}