Skip to content
Snippets Groups Projects
RequestsReceivedViewModel.swift 11.4 KiB
Newer Older
Bruno Muniz's avatar
Bruno Muniz committed
import HUD
import UIKit
import Models
import Shared
import Combine
import Defaults
import XXModels
import XXClient
import DrawerFeature
import ReportingFeature
Bruno Muniz's avatar
Bruno Muniz committed
import CombineSchedulers
import DependencyInjection

import struct XXModels.Group

struct RequestReceived: Hashable, Equatable {
    var request: Request?
    var isHidden: Bool
    var leader: String?
}

Bruno Muniz's avatar
Bruno Muniz committed
final class RequestsReceivedViewModel {
    @Dependency var e2e: E2E
    @Dependency var database: Database
    @Dependency var groupManager: GroupChat
    @Dependency var userDiscovery: UserDiscovery
    @Dependency var reportingStatus: ReportingStatus
Bruno Muniz's avatar
Bruno Muniz committed

    @KeyObject(.isShowingHiddenRequests, defaultValue: false) var isShowingHiddenRequests: Bool

    var hudPublisher: AnyPublisher<HUDStatus, Never> {
        hudSubject.eraseToAnyPublisher()
    }

    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<Contact, Never> {
        contactConfirmationSubject.eraseToAnyPublisher()
Bruno Muniz's avatar
Bruno Muniz committed
    }

    private var cancellables = Set<AnyCancellable>()
    private let updateSubject = CurrentValueSubject<Void, Never>(())
    private let verifyingSubject = PassthroughSubject<Void, Never>()
    private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
    private let groupConfirmationSubject = PassthroughSubject<Group, Never>()
    private let contactConfirmationSubject = PassthroughSubject<Contact, Never>()
    private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never>(.init())
Bruno Muniz's avatar
Bruno Muniz committed

    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: [
                .hidden,
                .verified,
                .verificationFailed,
                .verificationInProgress
            isBlocked: reportingStatus.isEnabled() ? false : nil,
            isBanned: reportingStatus.isEnabled() ? false : nil
        let groupStream = database.fetchGroupsPublisher(groupsQuery).assertNoFailure()
        let contactsStream = database.fetchContactsPublisher(contactsQuery).assertNoFailure()

        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,
Bruno Muniz's avatar
Bruno Muniz committed

            if self.isShowingHiddenRequests {
                snapshot.appendItems(receivedRequests.filter(\.isHidden), toSection: .hidden)
            }
Bruno Muniz's avatar
Bruno Muniz committed

            guard receivedRequests.filter({ $0.isHidden == false }).count > 0 else {
                snapshot.appendItems([RequestReceived(isHidden: false)], toSection: .appearing)
Bruno Muniz's avatar
Bruno Muniz committed
                return snapshot
Bruno Muniz's avatar
Bruno Muniz committed

            snapshot.appendItems(receivedRequests.filter { $0.isHidden == false }, toSection: .appearing)
            return snapshot
        }.sink(
            receiveCompletion: { _ in },
            receiveValue: { [unowned self] in itemsSubject.send($0) }
        ).store(in: &cancellables)
    }
Bruno Muniz's avatar
Bruno Muniz committed

    func didToggleHiddenRequestsSwitcher() {
        isShowingHiddenRequests.toggle()
        updateSubject.send()
    }
Bruno Muniz's avatar
Bruno Muniz committed

    func didTapStateButtonFor(request: Request) {
        guard case var .contact(contact) = request else { return }

        if request.status == .failedToVerify {
            backgroundScheduler.schedule { [weak self] in
                guard let self = self else { return }

                do {
                    contact.authStatus = .verificationInProgress
                    try self.database.saveContact(contact)

                    if contact.email == nil && contact.phone == nil {
                        let _ = try LookupUD.live(
                            e2eId: self.e2e.getId(),
                            udContact: self.userDiscovery.getContact(),
                            lookupId: contact.id,
                            callback: .init(handle: {
                                switch $0 {
                                case .success(let data):
                                    let ownershipResult = try! self.e2e.verifyOwnership(
                                        receivedContact: contact.marshaled!,
                                        verifiedContact: data,
                                        e2eId: self.e2e.getId()
                                    )

                                    if ownershipResult == true {
                                        contact.authStatus = .verified
                                        _ = try? self.database.saveContact(contact)
                                    } else {
                                        _ = try? self.database.deleteContact(contact)
                                    }
                                case .failure(let error):
                                    print("^^^ \(#file):\(#line)  \(error.localizedDescription)")
                                    contact.authStatus = .verificationFailed
                                    _ = try? self.database.saveContact(contact)
                                }
                            })
                        )
                    }
                } catch {
                    print("^^^ \(#file):\(#line)  \(error.localizedDescription)")
                    contact.authStatus = .verificationFailed
                    _ = try? self.database.saveContact(contact)
                }
Bruno Muniz's avatar
Bruno Muniz committed
            }
        } else if request.status == .verifying {
            verifyingSubject.send()
    func didRequestHide(group: Group) {
        if var group = try? database.fetchGroups(.init(id: [group.id])).first {
            group.authStatus = .hidden
            _ = try? database.saveGroup(group)
Bruno Muniz's avatar
Bruno Muniz committed

    func didRequestAccept(group: Group) {
Bruno Muniz's avatar
Bruno Muniz committed
        hudSubject.send(.on)
Bruno Muniz's avatar
Bruno Muniz committed

        backgroundScheduler.schedule { [weak self] in
            guard let self = self else { return }

Bruno Muniz's avatar
Bruno Muniz committed
            do {
                let trackedId = try self.groupManager
                    .getGroup(groupId: group.id)
                    .getTrackedID()

                try self.groupManager.joinGroup(trackedGroupId: trackedId)

                var group = group
                group.authStatus = .participating
                try self.database.saveGroup(group)

                self.hudSubject.send(.none)
                self.groupConfirmationSubject.send(group)
Bruno Muniz's avatar
Bruno Muniz committed
            } catch {
                self.hudSubject.send(.error(.init(with: error)))
    func fetchMembers(
        _ group: Group,
        _ completion: @escaping (Result<[DrawerTableCellModel], Error>) -> Void
    ) {
        if let info = try? database.fetchGroupInfos(.init(groupId: group.id)).first {
            database.fetchContactsPublisher(.init(id: Set(info.members.map(\.id))))
                .assertNoFailure()
                .sink { members in
                    let withUsername = members
                        .filter { $0.username != nil }
                        .map {
                            DrawerTableCellModel(
                                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(
                                title: "Fetching username...",
                                image: $0.photo,
                                isCreator: $0.id == group.leaderId,
                                isConnection: $0.authStatus == .friend
                            )
                        }

                    completion(.success(withUsername + withoutUsername))
                }.store(in: &cancellables)
        }
Bruno Muniz's avatar
Bruno Muniz committed

    func didRequestHide(contact: Contact) {
        if var contact = try? database.fetchContacts(.init(id: [contact.id])).first {
            contact.authStatus = .hidden
            _ = try? database.saveContact(contact)
    func didRequestAccept(contact: Contact, nickname: String? = nil) {
Bruno Muniz's avatar
Bruno Muniz committed
        hudSubject.send(.on)

        var contact = contact
        contact.authStatus = .confirming
        contact.nickname = nickname ?? contact.username
Bruno Muniz's avatar
Bruno Muniz committed

        backgroundScheduler.schedule { [weak self] in
            guard let self = self else { return }

                try self.database.saveContact(contact)

                let _ = try self.e2e.confirmReceivedRequest(partnerContact: contact.id)
                contact.authStatus = .friend
                try self.database.saveContact(contact)

                self.hudSubject.send(.none)
                self.contactConfirmationSubject.send(contact)
            } catch {
                contact.authStatus = .confirmationFailed
                _ = try? self.database.saveContact(contact)
                self.hudSubject.send(.error(.init(with: error)))
    func groupChatWith(group: Group) -> GroupInfo {
        guard let info = try? database.fetchGroupInfos(.init(groupId: group.id)).first else {
            fatalError()
        }