import UIKit import Shared import AppCore import Combine import Defaults import XXModels import XXClient import Dependencies import DrawerFeature import ReportingFeature import CombineSchedulers import XXMessengerClient import struct XXModels.Group struct RequestReceived: Hashable, Equatable { var request: Request? var isHidden: Bool var leader: String? } final class RequestsReceivedViewModel { @Dependency(\.app.dbManager) var dbManager @Dependency(\.app.messenger) var messenger @Dependency(\.hudManager) var hudManager @Dependency(\.reportingStatus) var reportingStatus @KeyObject(.isShowingHiddenRequests, defaultValue: false) var isShowingHiddenRequests: Bool var verifyingPublisher: AnyPublisher<Void, Never> { verifyingSubject.eraseToAnyPublisher() } var itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never> { itemsSubject.eraseToAnyPublisher() } var groupConfirmationPublisher: AnyPublisher<Group, Never> { groupConfirmationSubject.eraseToAnyPublisher() } var contactConfirmationPublisher: AnyPublisher<XXModels.Contact, Never> { contactConfirmationSubject.eraseToAnyPublisher() } private var cancellables = Set<AnyCancellable>() private let updateSubject = CurrentValueSubject<Void, Never>(()) private let verifyingSubject = PassthroughSubject<Void, Never>() private let groupConfirmationSubject = PassthroughSubject<Group, Never>() private let contactConfirmationSubject = PassthroughSubject<XXModels.Contact, Never>() private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never>(.init()) var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() init() { let groupsQuery = Group.Query( authStatus: [ .hidden, .pending ], isLeaderBlocked: reportingStatus.isEnabled() ? false : nil, isLeaderBanned: reportingStatus.isEnabled() ? false : nil ) let contactsQuery = Contact.Query( authStatus: [ .friend, .hidden, .verified, .verificationFailed, .verificationInProgress ], isBlocked: reportingStatus.isEnabled() ? false : nil, isBanned: reportingStatus.isEnabled() ? false : nil ) let groupStream = try! dbManager.getDB() .fetchGroupsPublisher(groupsQuery) .replaceError(with: []) let contactsStream = try! dbManager.getDB() .fetchContactsPublisher(contactsQuery) .replaceError(with: []) Publishers.CombineLatest3( groupStream, contactsStream, updateSubject.eraseToAnyPublisher() ) .subscribe(on: DispatchQueue.main) .receive(on: DispatchQueue.global()) .map { [unowned self] data -> NSDiffableDataSourceSnapshot<Section, RequestReceived> in var snapshot = NSDiffableDataSourceSnapshot<Section, RequestReceived>() snapshot.appendSections([.appearing, .hidden]) let contactsFilteringFriends = data.1.filter { $0.authStatus != .friend } let requests = data.0.map(Request.group) + contactsFilteringFriends.map(Request.contact) let receivedRequests = requests.map { request -> RequestReceived in switch request { case let .group(group): func leaderName() -> String { if let leader = data.1.first(where: { $0.id == group.leaderId }) { return (leader.nickname ?? leader.username) ?? "Leader is not a friend" } else { return "[Error retrieving leader]" } } return RequestReceived( request: request, isHidden: group.authStatus == .hidden, leader: leaderName() ) case let .contact(contact): return RequestReceived( request: request, isHidden: contact.authStatus == .hidden, leader: nil ) } } if self.isShowingHiddenRequests { snapshot.appendItems(receivedRequests.filter(\.isHidden), toSection: .hidden) } guard receivedRequests.filter({ $0.isHidden == false }).count > 0 else { snapshot.appendItems([RequestReceived(isHidden: false)], toSection: .appearing) return snapshot } snapshot.appendItems(receivedRequests.filter { $0.isHidden == false }, toSection: .appearing) return snapshot }.sink( receiveCompletion: { _ in }, receiveValue: { [unowned self] in itemsSubject.send($0) } ).store(in: &cancellables) } func didToggleHiddenRequestsSwitcher() { isShowingHiddenRequests.toggle() updateSubject.send() } func didTapStateButtonFor(request: Request) { guard case var .contact(contact) = request else { return } if request.status == .failedToVerify { backgroundScheduler.schedule { [weak self] in guard let self else { return } do { contact.authStatus = .verificationInProgress try self.dbManager.getDB().saveContact(contact) print(">>> [messenger.verifyContact] will start") if try self.messenger.verifyContact(XXClient.Contact.live(contact.marshaled!)) { print(">>> [messenger.verifyContact] verified") contact.authStatus = .verified contact = try self.dbManager.getDB().saveContact(contact) } else { print(">>> [messenger.verifyContact] is fake") try self.dbManager.getDB().deleteContact(contact) } } catch { print(">>> [messenger.verifyContact] thrown an exception: \(error.localizedDescription)") contact.authStatus = .verificationFailed _ = try? self.dbManager.getDB().saveContact(contact) } } } else if request.status == .verifying { verifyingSubject.send() } } func didRequestHide(group: Group) { if var group = try? dbManager.getDB().fetchGroups(.init(id: [group.id])).first { group.authStatus = .hidden _ = try? dbManager.getDB().saveGroup(group) } } func didRequestAccept(group: Group) { hudManager.show() backgroundScheduler.schedule { [weak self] in guard let self else { return } do { try self.messenger.groupChat()!.joinGroup(serializedGroupData: group.serialized) var group = group group.authStatus = .participating try self.dbManager.getDB().saveGroup(group) self.hudManager.hide() self.groupConfirmationSubject.send(group) } catch { self.hudManager.show(.init(error: error)) } } } func fetchMembers( _ group: Group, _ completion: @escaping (Result<[DrawerTableCellModel], Error>) -> Void ) { if let info = try? dbManager.getDB().fetchGroupInfos(.init(groupId: group.id)).first { try! dbManager.getDB().fetchContactsPublisher(.init(id: Set(info.members.map(\.id)))) .replaceError(with: []) .sink { members in let withUsername = members .filter { $0.username != nil } .map { DrawerTableCellModel( id: $0.id, title: $0.nickname ?? $0.username!, image: $0.photo, isCreator: $0.id == group.leaderId, isConnection: $0.authStatus == .friend ) } let withoutUsername = members .filter { $0.username == nil } .map { DrawerTableCellModel( id: $0.id, title: "Fetching username...", image: $0.photo, isCreator: $0.id == group.leaderId, isConnection: $0.authStatus == .friend ) } completion(.success(withUsername + withoutUsername)) }.store(in: &cancellables) } } func didRequestHide(contact: XXModels.Contact) { if var contact = try? dbManager.getDB().fetchContacts(.init(id: [contact.id])).first { contact.authStatus = .hidden _ = try? dbManager.getDB().saveContact(contact) } } func didRequestAccept(contact: XXModels.Contact, nickname: String? = nil) { hudManager.show() var contact = contact contact.authStatus = .confirming contact.nickname = nickname ?? contact.username backgroundScheduler.schedule { [weak self] in guard let self else { return } do { try self.dbManager.getDB().saveContact(contact) let _ = try self.messenger.e2e.get()!.confirmReceivedRequest(partner: .live(contact.marshaled!)) contact.isRecent = true contact.authStatus = .friend try self.dbManager.getDB().saveContact(contact) self.hudManager.hide() self.contactConfirmationSubject.send(contact) } catch { contact.authStatus = .confirmationFailed _ = try? self.dbManager.getDB().saveContact(contact) self.hudManager.show(.init(error: error)) } } } func groupChatWith(group: Group) -> GroupInfo { guard let info = try? dbManager.getDB().fetchGroupInfos(.init(groupId: group.id)).first else { fatalError() } return info } }