Swiftgram/submodules/InviteLinksUI/Sources/InviteRequestsController.swift
2022-12-13 17:42:32 +04:00

435 lines
20 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AlertUI
import PresentationDataUtils
import AppBundle
import ContextUI
import TelegramStringFormatting
import ItemListPeerActionItem
import ItemListPeerItem
import ShareController
import UndoUI
private final class InviteRequestsControllerArguments {
let context: AccountContext
let openLinks: () -> Void
let openPeer: (EnginePeer) -> Void
let approveRequest: (EnginePeer) -> Void
let denyRequest: (EnginePeer) -> Void
let peerContextAction: (EnginePeer, ASDisplayNode, ContextGesture?) -> Void
init(context: AccountContext, openLinks: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, approveRequest: @escaping (EnginePeer) -> Void, denyRequest: @escaping (EnginePeer) -> Void, peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void) {
self.context = context
self.openLinks = openLinks
self.openPeer = openPeer
self.approveRequest = approveRequest
self.denyRequest = denyRequest
self.peerContextAction = peerContextAction
}
}
private enum InviteRequestsSection: Int32 {
case header
case requests
}
private enum InviteRequestsEntry: ItemListNodeEntry {
case header(PresentationTheme, String)
case requestsHeader(PresentationTheme, String)
case request(Int32, PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerInvitationImportersState.Importer, Bool)
var section: ItemListSectionId {
switch self {
case .header:
return InviteRequestsSection.header.rawValue
case .requestsHeader, .request:
return InviteRequestsSection.requests.rawValue
}
}
var stableId: Int32 {
switch self {
case .header:
return 0
case .requestsHeader:
return 1
case let .request(index, _, _, _, _, _):
return 2 + index
}
}
static func ==(lhs: InviteRequestsEntry, rhs: InviteRequestsEntry) -> Bool {
switch lhs {
case let .header(lhsTheme, lhsText):
if case let .header(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .requestsHeader(lhsTheme, lhsText):
if case let .requestsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .request(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsImporter, lhsIsGroup):
if case let .request(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsImporter, rhsIsGroup) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, lhsImporter == rhsImporter, lhsIsGroup == rhsIsGroup {
return true
} else {
return false
}
}
}
static func <(lhs: InviteRequestsEntry, rhs: InviteRequestsEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! InviteRequestsControllerArguments
switch self {
case let .header(theme, text):
return InviteLinkHeaderItem(context: arguments.context, theme: theme, text: text, animationName: "Requests", sectionId: self.section, linkAction: { _ in
arguments.openLinks()
})
case let .requestsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .request(_, _, dateTimeFormat, nameDisplayOrder, importer, isGroup):
return ItemListInviteRequestItem(context: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, importer: importer, isGroup: isGroup, sectionId: self.section, style: .blocks, tapAction: {
if let peer = importer.peer.peer.flatMap({ EnginePeer($0) }) {
arguments.openPeer(peer)
}
}, addAction: {
if let peer = importer.peer.peer.flatMap({ EnginePeer($0) }) {
arguments.approveRequest(peer)
}
}, dismissAction: {
if let peer = importer.peer.peer.flatMap({ EnginePeer($0) }) {
arguments.denyRequest(peer)
}
}, contextAction: { node, gesture in
if let peer = importer.peer.peer.flatMap({ EnginePeer($0) }) {
arguments.peerContextAction(peer, node, gesture)
}
})
}
}
}
private func inviteRequestsControllerEntries(presentationData: PresentationData, peer: EnginePeer?, importers: [PeerInvitationImportersState.Importer]?, count: Int32, isGroup: Bool) -> [InviteRequestsEntry] {
var entries: [InviteRequestsEntry] = []
if let importers = importers, !importers.isEmpty {
let helpText: String
if case let .channel(peer) = peer, case .broadcast = peer.info {
helpText = presentationData.strings.MemberRequests_DescriptionChannel
} else {
helpText = presentationData.strings.MemberRequests_DescriptionGroup
}
entries.append(.header(presentationData.theme, helpText))
entries.append(.requestsHeader(presentationData.theme, presentationData.strings.MemberRequests_PeopleRequested(count).uppercased()))
var index: Int32 = 0
for importer in importers {
entries.append(.request(index, presentationData.theme, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, importer, isGroup))
index += 1
}
}
return entries
}
private struct InviteRequestsControllerState: Equatable {
var searchingMembers: Bool
}
public func inviteRequestsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, existingContext: PeerInvitationImportersContext? = nil) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var presentInGlobalOverlayImpl: ((ViewController) -> Void)?
var navigateToProfileImpl: ((EnginePeer) -> Void)?
var navigateToChatImpl: ((EnginePeer) -> Void)?
var dismissInputImpl: (() -> Void)?
var dismissTooltipsImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
if let existingContext = existingContext {
existingContext.reload()
}
let statePromise = ValuePromise(InviteRequestsControllerState(searchingMembers: false), ignoreRepeated: true)
let stateValue = Atomic(value: InviteRequestsControllerState(searchingMembers: false))
let updateState: ((InviteRequestsControllerState) -> InviteRequestsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let updateDisposable = MetaDisposable()
actionsDisposable.add(updateDisposable)
let importersContext = existingContext ?? context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .requests(query: nil))
let approveRequestImpl: (EnginePeer) -> Void = { peer in
importersContext.update(peer.id, action: .approve)
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).start(next: { chatPeer in
guard let chatPeer = chatPeer else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let string: String
if case let .channel(channel) = chatPeer, case .broadcast = channel.info {
string = presentationData.strings.MemberRequests_UserAddedToChannel(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string
} else {
string = presentationData.strings.MemberRequests_UserAddedToGroup(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string
}
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: context, peer: peer, text: string, action: nil, duration: 3), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
})
}
let denyRequestImpl: (EnginePeer) -> Void = { peer in
importersContext.update(peer.id, action: .deny)
}
let arguments = InviteRequestsControllerArguments(context: context, openLinks: {
let controller = inviteLinkListController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, admin: nil)
pushControllerImpl?(controller)
}, openPeer: { peer in
navigateToProfileImpl?(peer)
}, approveRequest: { peer in
approveRequestImpl(peer)
}, denyRequest: { peer in
denyRequestImpl(peer)
}, peerContextAction: { peer, node, gesture in
guard let node = node as? ContextExtractedContentContainingNode else {
return
}
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).start(next: { chatPeer in
guard let chatPeer = chatPeer else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let addString: String
if case let .channel(channel) = chatPeer, case .broadcast = channel.info {
addString = presentationData.strings.MemberRequests_AddToChannel
} else {
addString = presentationData.strings.MemberRequests_AddToGroup
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: addString, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
approveRequestImpl(peer)
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ContactList_Context_SendMessage, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Message"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
navigateToChatImpl?(peer)
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MemberRequests_Dismiss, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor)
}, action: { _, f in
f(.dismissWithoutContent)
Queue.mainQueue().after(0.3, {
denyRequestImpl(peer)
})
})))
let dismissPromise = ValuePromise<Bool>(false)
let source = InviteRequestsContextExtractedContentSource(sourceNode: node, keepInPlace: false, blurBackground: true, centerVertically: true, shouldBeDismissed: dismissPromise.get())
// sourceNode.requestDismiss = {
// dismissPromise.set(true)
// }
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
presentInGlobalOverlayImpl?(contextController)
})
})
let previousEntries = Atomic<[InviteRequestsEntry]>(value: [])
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(queue: .mainQueue(),
presentationData,
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
),
importersContext.state,
statePromise.get()
)
|> map { presentationData, peer, importersState, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
var isGroup = true
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
}
var emptyStateItem: ItemListControllerEmptyStateItem?
if importersState.hasLoadedOnce && importersState.importers.isEmpty {
emptyStateItem = InviteRequestsEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings, isGroup: isGroup)
}
let entries = inviteRequestsControllerEntries(presentationData: presentationData, peer: peer, importers: importersState.hasLoadedOnce ? importersState.importers : nil, count: importersState.count, isGroup: isGroup)
let previousEntries = previousEntries.swap(entries)
let crossfade = !previousEntries.isEmpty && entries.isEmpty
let animateChanges = (!previousEntries.isEmpty && !entries.isEmpty) && previousEntries.count != entries.count
let rightNavigationButton: ItemListNavigationButton?
if !importersState.importers.isEmpty {
rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: {
updateState { state in
var updatedState = state
updatedState.searchingMembers = true
return updatedState
}
})
} else {
rightNavigationButton = nil
}
var searchItem: ItemListControllerSearch?
if state.searchingMembers && !importersState.importers.isEmpty {
searchItem = InviteRequestsSearchItem(context: context, peerId: peerId, cancel: {
updateState { state in
var updatedState = state
updatedState.searchingMembers = false
return updatedState
}
}, openPeer: { peer in
arguments.openPeer(peer)
}, approveRequest: { peer in
arguments.approveRequest(peer)
}, denyRequest: { peer in
arguments.denyRequest(peer)
}, navigateToChat: { peer in
navigateToChatImpl?(peer)
}, pushController: { c in
pushControllerImpl?(c)
}, dismissInput: {
dismissInputImpl?()
}, presentInGlobalOverlay: { c in
presentInGlobalOverlayImpl?(c)
})
}
let title: ItemListControllerTitle = .text(presentationData.strings.MemberRequests_Title)
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges, scrollEnabled: emptyStateItem == nil)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.willDisappear = { _ in
dismissTooltipsImpl?()
}
controller.didDisappear = { [weak controller] _ in
controller?.clearItemNodesHighlight(animated: true)
}
controller.visibleBottomContentOffsetChanged = { offset in
if case let .known(value) = offset, value < 40.0 {
importersContext.loadMore()
}
}
pushControllerImpl = { [weak controller] c in
if let controller = controller {
(controller.navigationController as? NavigationController)?.pushViewController(c, animated: true)
}
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
presentInGlobalOverlayImpl = { [weak controller] c in
if let controller = controller {
controller.presentInGlobalOverlay(c)
}
}
navigateToProfileImpl = { [weak controller] peer in
if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(controller)
}
}
navigateToChatImpl = { [weak controller] peer in
if let navigationController = controller?.navigationController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always))
}
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
dismissTooltipsImpl = { [weak controller] in
controller?.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
})
controller?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
return true
})
}
return controller
}
final class InviteRequestsContextExtractedContentSource: ContextExtractedContentSource {
var keepInPlace: Bool
let ignoreContentTouches: Bool = false
let blurBackground: Bool
private let sourceNode: ContextExtractedContentContainingNode
var centerVertically: Bool
var shouldBeDismissed: Signal<Bool, NoError>
init(sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool, centerVertically: Bool, shouldBeDismissed: Signal<Bool, NoError>) {
self.sourceNode = sourceNode
self.keepInPlace = keepInPlace
self.blurBackground = blurBackground
self.centerVertically = centerVertically
self.shouldBeDismissed = shouldBeDismissed
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}