Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
folium-app
GitHub Repository: folium-app/Folium
Path: blob/a-new-beginning/Folium-iOS/Controllers/CytrusMultiplayerRoomsController.swift
2 views
//
//  CytrusMultiplayerRoomsController.swift
//  Folium-iOS
//
//  Created by Jarrod Norwell on 21/10/2025.
//

import Cytrus
import Foundation
import UIKit

class CytrusMultiplayerRoomsController : UICollectionViewController {
    var dataSource: UICollectionViewDiffableDataSource<String, CytrusRoom>? = nil
    var snapshot: NSDiffableDataSourceSnapshot<String, CytrusRoom>? = nil
    
    var chatController: CytrusMultiplayerChatController? = nil
    
    var rightBarButtonItem: UIBarButtonItem? {
        if let connectedRoom = applicationModel.cytrus.multiplayer.connectedRoom {
            .init(image: .init(systemName: "bubble.left"), primaryAction: .init { _ in
                self.chatController = .init(applicationModel: self.applicationModel,
                                            room: connectedRoom,
                                            collectionViewLayout: self.applicationModel.layoutManager.list)
                guard let chatController = self.chatController else {
                    return
                }
                
                let navigationController: UINavigationController = .init(rootViewController: chatController)
                
                if let sheetPresentationController = navigationController.sheetPresentationController {
                    let custom: UISheetPresentationController.Detent = .custom { context in self.view.safeAreaInsets.top + self.view.safeAreaInsets.bottom }
                    let medium: UISheetPresentationController.Detent = .medium()
                    let large: UISheetPresentationController.Detent = .large()
                    
                    sheetPresentationController.detents = [custom, medium, large]
                    sheetPresentationController.largestUndimmedDetentIdentifier = custom.identifier
                    sheetPresentationController.prefersGrabberVisible = true
                    if #available(iOS 26.1, *) {
                        sheetPresentationController.backgroundEffect = UIGlassEffect(style: .regular)
                    }
                    
                    self.present(navigationController, animated: true)
                }
            })
        } else {
            nil
        }
    }
    
    var applicationModel: ApplicationModel
    var game: NewCytrusGame
    init(_ applicationModel: ApplicationModel, _ game: NewCytrusGame) {
        self.applicationModel = applicationModel
        self.game = game
        super.init(collectionViewLayout: applicationModel.layoutManager.multiplayer)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        if let navigationController {
            navigationController.navigationBar.prefersLargeTitles = true
        }
        if #available(iOS 17, *) {
            navigationItem.largeTitleDisplayMode = .inline
        }
        navigationItem.style = .browser
        if #available(iOS 26, *) {
            navigationItem.largeTitle = .localized(for: .multiplayer)
            navigationItem.title = navigationItem.largeTitle
            navigationItem.largeSubtitle = "Fetching rooms..."
            navigationItem.subtitle = navigationItem.largeSubtitle
        } else {
            navigationItem.title = .localized(for: .multiplayer)
        }
        navigationItem.leftBarButtonItem = .init(systemItem: .close, primaryAction: .init { _ in
            let controller = self.presentingViewController
            self.dismiss(animated: true) {
                guard let controller = controller as? CytrusController else {
                    return
                }
                
                controller.applicationModel.cytrus.pause(false)
            }
        })
        navigationItem.rightBarButtonItem = rightBarButtonItem
        
        let supplementaryCellRegistration: UICollectionView.SupplementaryRegistration<UICollectionViewListCell> = .init(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in
            var contentConfiguration = UIListContentConfiguration.extraProminentInsetGroupedHeader()
            if let dataSource = self.dataSource, let game = dataSource.sectionIdentifier(for: indexPath.section) {
                contentConfiguration.textProperties.numberOfLines = 1
                contentConfiguration.text = game.capitalized
            }
            supplementaryView.contentConfiguration = contentConfiguration
        }
        
        let cellRegistration: UICollectionView.CellRegistration<CytrusMultplayerRoomCell, CytrusRoom> = .init { cell, indexPath, room in
            cell.set(room, isCurrent: self.applicationModel.cytrus.multiplayer.connectedRoom?.name == room.name)
        }
        
        dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
        }
        guard let dataSource else {
            return
        }
        
        dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
            switch elementKind {
            case UICollectionView.elementKindSectionHeader:
                collectionView.dequeueConfiguredReusableSupplementary(using: supplementaryCellRegistration, for: indexPath)
            default: nil
            }
        }
        
        applicationModel.cytrus.multiplayer.delegate = self
        
        Task {
            await populateRooms()
        }
    }
    
    func populateRooms() async {
        snapshot = .init()
        guard let dataSource, var snapshot else {
            return
        }
        
        let identifier: String? = if let identifier = game.information.identifier { "\(identifier)" } else { nil }
        
        let rooms = applicationModel.cytrus.multiplayer.availableRooms(for: identifier)
        
        if #available(iOS 26, *) {
            if rooms.isEmpty {
                navigationItem.largeSubtitle = "No rooms available"
                navigationItem.subtitle = navigationItem.largeSubtitle
            } else {
                navigationItem.largeSubtitle = "\(rooms.count) room\(rooms.count == 1 ? "" : "s") available"
                navigationItem.subtitle = navigationItem.largeSubtitle
            }
        }
        
        snapshot.appendSections(Array(Set(rooms.map { $0.preferredGame })).sorted())
        snapshot.sectionIdentifiers.forEach { preferredGame in
            snapshot.appendItems(rooms.filter { $0.preferredGame == preferredGame }, toSection: preferredGame)
        }
        await dataSource.apply(snapshot)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        guard let connectedRoom = applicationModel.cytrus.multiplayer.connectedRoom else {
            return
        }
        
        chatController = .init(applicationModel: applicationModel,
                               room: connectedRoom,
                               collectionViewLayout: applicationModel.layoutManager.list)
        guard let chatController else {
            return
        }
        
        let navigationController: UINavigationController = .init(rootViewController: chatController)
        
        if let sheetPresentationController = navigationController.sheetPresentationController {
            let custom: UISheetPresentationController.Detent = .custom { context in self.view.safeAreaInsets.top + self.view.safeAreaInsets.bottom }
            let medium: UISheetPresentationController.Detent = .medium()
            let large: UISheetPresentationController.Detent = .large()
            
            sheetPresentationController.detents = [custom, medium, large]
            sheetPresentationController.largestUndimmedDetentIdentifier = custom.identifier
            sheetPresentationController.prefersGrabberVisible = true
            if #available(iOS 26.1, *) {
                sheetPresentationController.backgroundEffect = UIGlassEffect(style: .regular)
            }
            
            present(navigationController, animated: true) {
                self.navigationItem.rightBarButtonItem = self.rightBarButtonItem
            }
        }
    }
    
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let dataSource, let room = dataSource.itemIdentifier(for: indexPath) else {
            return
        }
        
        if applicationModel.cytrus.multiplayer.connectedRoom?.name == room.name {
            let alertController: UIAlertController = .init(title: "Disconnect from \(room.name)?", message: nil, preferredStyle: .alert)
            alertController.addAction(.init(title: .localized(for: .dismiss), style: .cancel))
            alertController.addAction(.init(title: "Disconnect", style: .default) { _ in
                self.applicationModel.cytrus.multiplayer.disconnect()
                self.navigationItem.rightBarButtonItem = nil
                Task {
                    await self.populateRooms()
                }
            })
            alertController.preferredAction = alertController.actions.last
            present(alertController, animated: true)
        } else {
            let alertController: UIAlertController = .init(title: "Connecting to \(room.name)",
                                                           message: room.passwordLocked ? "Enter the password for this room before tapping Connect" : nil, preferredStyle: .alert)
            
            alertController.addTextField { textField in
                textField.placeholder = "Username"
            }
            if room.passwordLocked {
                alertController.addTextField { textField in
                    textField.placeholder = "Password"
                }
            }
            
            alertController.addAction(.init(title: .localized(for: .dismiss), style: .cancel))
            alertController.addAction(.init(title: "Connect", style: .default) { _ in
                guard let textFields = alertController.textFields,
                      let usernameTextField = textFields.first,
                      let username = usernameTextField.text else {
                    return
                }
                
                var password: String? = nil
                if room.passwordLocked {
                    password = textFields.last?.text
                }
                
                self.applicationModel.cytrus.multiplayer.connect(to: room, with: username, and: password)
            })
            alertController.preferredAction = alertController.actions.last
            
            present(alertController, animated: true) {
                self.navigationItem.rightBarButtonItem = self.rightBarButtonItem
            }
        }
    }
}

extension CytrusMultiplayerRoomsController  : CytrusMultiplayerManagerDelegate {
    func didReceiveChatEntry(_ entry: CytrusNetworkChatEntry) {
        print(entry.nickname, entry.username, entry.message)
        guard let chatController else {
            return
        }
        
        chatController.receivedChatEntry(entry)
    }
    
    func didReceiveError(_ error: CytrusNetworkRoomMemberError) {
        print(error.string)
    }
    
    func didReceiveState(_ state: CytrusNetworkRoomMemberState) {
        switch state {
        case .joined:
            Task {
                chatController = .init(applicationModel: applicationModel,
                                       room: applicationModel.cytrus.multiplayer.connectedRoom,
                                       collectionViewLayout: applicationModel.layoutManager.list)
                guard let chatController else {
                    return
                }
                
                let navigationController: UINavigationController = .init(rootViewController: chatController)
                
                if let sheetPresentationController = navigationController.sheetPresentationController {
                    let custom: UISheetPresentationController.Detent = .custom { context in self.view.safeAreaInsets.top + self.view.safeAreaInsets.bottom }
                    let medium: UISheetPresentationController.Detent = .medium()
                    let large: UISheetPresentationController.Detent = .large()
                    
                    sheetPresentationController.detents = [custom, medium, large]
                    sheetPresentationController.largestUndimmedDetentIdentifier = custom.identifier
                    sheetPresentationController.prefersGrabberVisible = true
                    if #available(iOS 26.1, *) {
                        sheetPresentationController.backgroundEffect = UIGlassEffect(style: .regular)
                    }
                    
                    present(navigationController, animated: true) {
                        self.navigationItem.rightBarButtonItem = self.rightBarButtonItem
                    }
                }
                
                await populateRooms()
            }
        default:
            navigationItem.rightBarButtonItem = nil
        }
    }
}