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
}
}
}