diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt index a123511..c30e138 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -73,9 +74,13 @@ import ru.risdeveau.pixeldragon.push.MatrixUnifiedPushManager import ru.risdeveau.pixeldragon.ui.item.Avatar import ru.risdeveau.pixeldragon.ui.layout.AccountSettingsScreen import ru.risdeveau.pixeldragon.ui.layout.AppearanceSettingsScreen +import ru.risdeveau.pixeldragon.ui.layout.CreateRoomScreen import ru.risdeveau.pixeldragon.ui.layout.DevicesSettingsScreen +import ru.risdeveau.pixeldragon.ui.layout.JoinRoomScreen import ru.risdeveau.pixeldragon.ui.layout.NotificationsSettingsScreen import ru.risdeveau.pixeldragon.ui.layout.Room +import ru.risdeveau.pixeldragon.ui.layout.RoomActionsRoutes +import ru.risdeveau.pixeldragon.ui.layout.RoomActionsSheet import ru.risdeveau.pixeldragon.ui.layout.RoomList import ru.risdeveau.pixeldragon.ui.layout.SettingsMainScreen import ru.risdeveau.pixeldragon.ui.layout.SettingsRoutes @@ -108,6 +113,18 @@ class MainActivity : ComponentActivity() { val navController = rememberNavController() val syncState by client!!.api.sync.currentSyncState .collectAsState(initial = SyncState.STOPPED) + val backStackEntry by navController.currentBackStackEntryAsState() + val route = backStackEntry?.destination?.route + var showRoomActionsSheet by remember { mutableStateOf(false) } + + if (showRoomActionsSheet) { + RoomActionsSheet( + onDismiss = { showRoomActionsSheet = false }, + onJoinRoom = { navController.navigate(RoomActionsRoutes.JoinRoom) }, + onCreateRoom = { navController.navigate(RoomActionsRoutes.CreateRoom) }, + onCreateSpace = { navController.navigate(RoomActionsRoutes.CreateSpace) }, + ) + } Scaffold( modifier = Modifier.fillMaxSize(), @@ -117,9 +134,16 @@ class MainActivity : ComponentActivity() { syncState = syncState, ) }, + floatingActionButton = { + if (route == RoomActionsRoutes.Rooms) { + FloatingActionButton(onClick = { showRoomActionsSheet = true }) { + Text("+") + } + } + }, ) { innerPadding -> - NavHost(navController = navController, startDestination = "rooms") { - composable("rooms") { + NavHost(navController = navController, startDestination = RoomActionsRoutes.Rooms) { + composable(RoomActionsRoutes.Rooms) { RoomList(Modifier.padding(innerPadding), navController) } composable( @@ -140,6 +164,41 @@ class MainActivity : ComponentActivity() { ) } + composable(RoomActionsRoutes.JoinRoom) { + JoinRoomScreen( + modifier = Modifier.padding(innerPadding), + onJoined = { roomId -> + navController.navigate("room/${roomId.full}") { + popUpTo(RoomActionsRoutes.Rooms) + } + }, + ) + } + + composable(RoomActionsRoutes.CreateRoom) { + CreateRoomScreen( + modifier = Modifier.padding(innerPadding), + isSpace = false, + onCreated = { roomId -> + navController.navigate("room/${roomId.full}") { + popUpTo(RoomActionsRoutes.Rooms) + } + }, + ) + } + + composable(RoomActionsRoutes.CreateSpace) { + CreateRoomScreen( + modifier = Modifier.padding(innerPadding), + isSpace = true, + onCreated = { roomId -> + navController.navigate("space/${roomId.full}") { + popUpTo(RoomActionsRoutes.Rooms) + } + }, + ) + } + navigation( route = SettingsRoutes.Graph, startDestination = SettingsRoutes.Main, @@ -216,6 +275,7 @@ private fun PixelDragonTopBar( val route = backStackEntry?.destination?.route val rid = backStackEntry?.arguments?.getString("rid") val isRoomLikeScreen = route == "room/{rid}" || route == "space/{rid}" + val roomActionTitle = RoomActionsRoutes.titleFor(route) val isSettingsScreen = route == SettingsRoutes.Graph || route?.startsWith("settings/") == true val roomsFlow = remember(client) { @@ -240,6 +300,14 @@ private fun PixelDragonTopBar( ) } + roomActionTitle != null -> { + RoomTopBar( + room = null, + fallbackTitle = roomActionTitle, + onBack = { navController.popBackStack() }, + ) + } + isSettingsScreen -> { RoomTopBar( room = null, diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/CreateRoom.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/CreateRoom.kt new file mode 100755 index 0000000..47729a9 --- /dev/null +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/CreateRoom.kt @@ -0,0 +1,195 @@ +/* + * Created by sweetbread + * Copyright (c) 2026. All rights reserved. + */ + +package ru.risdeveau.pixeldragon.ui.layout + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import de.connect2x.trixnity.clientserverapi.model.room.CreateRoom +import de.connect2x.trixnity.clientserverapi.model.room.DirectoryVisibility +import de.connect2x.trixnity.core.model.RoomAliasId +import de.connect2x.trixnity.core.model.RoomId +import de.connect2x.trixnity.core.model.events.m.room.CreateEventContent +import kotlinx.coroutines.launch +import ru.risdeveau.pixeldragon.client + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreateRoomScreen( + modifier: Modifier = Modifier, + isSpace: Boolean, + onCreated: (RoomId) -> Unit, +) { + val matrixClient = client!! + val scope = rememberCoroutineScope() + + var name by remember { mutableStateOf("") } + var topic by remember { mutableStateOf("") } + var alias by remember { mutableStateOf("") } + var isPublic by remember { mutableStateOf(false) } + var isCreating by remember { mutableStateOf(false) } + var error by remember { mutableStateOf(null) } + + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text( + text = if (isSpace) "New space" else "New room", + style = MaterialTheme.typography.headlineSmall, + ) + + Text( + text = if (isSpace) { + "Spaces are useful for grouping related rooms." + } else { + "Create a room, then invite people or publish it in the directory." + }, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = name, + onValueChange = { name = it }, + singleLine = true, + label = { Text(if (isSpace) "Space name" else "Room name") }, + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = topic, + onValueChange = { topic = it }, + minLines = 2, + label = { Text("Topic") }, + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = alias, + onValueChange = { alias = it.trim() }, + singleLine = true, + label = { Text("Local alias, optional") }, + supportingText = { + Text("Example: pixeldragon creates #pixeldragon:${matrixClient.userId.domain}") + }, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Publish in room directory") + Text( + text = "People can find it in public search. Join rules are still controlled by the server.", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall, + ) + } + + Switch( + checked = isPublic, + onCheckedChange = { isPublic = it }, + ) + } + + error?.let { + Text( + text = it, + color = MaterialTheme.colorScheme.error, + ) + } + + Spacer(Modifier.height(4.dp)) + + Button( + modifier = Modifier.fillMaxWidth(), + enabled = !isCreating && name.isNotBlank(), + onClick = { + isCreating = true + error = null + + scope.launch { + val aliasValue = alias.trim() + val roomAliasId = aliasValue.toRoomAliasIdOrNull(matrixClient.userId.domain) + + if (aliasValue.isNotEmpty() && roomAliasId == null) { + error = "Invalid alias" + isCreating = false + return@launch + } + + val result = matrixClient.api.room.createRoom( + visibility = if (isPublic) DirectoryVisibility.PUBLIC else DirectoryVisibility.PRIVATE, + roomAliasId = roomAliasId, + name = name.trim(), + topic = topic.trim().takeIf { it.isNotEmpty() }, + preset = if (isPublic) CreateRoom.Request.Preset.PUBLIC else CreateRoom.Request.Preset.PRIVATE, + creationContent = if (isSpace) { + CreateEventContent(type = CreateEventContent.RoomType.Space) + } else { + null + }, + ) + + result.fold( + onSuccess = { roomId -> onCreated(roomId) }, + onFailure = { throwable -> + error = throwable.message ?: "Could not create ${if (isSpace) "space" else "room"}" + }, + ) + + isCreating = false + } + }, + ) { + if (isCreating) { + CircularProgressIndicator() + } else { + Text(if (isSpace) "Create space" else "Create room") + } + } + } +} + +private fun String.toRoomAliasIdOrNull(defaultDomain: String): RoomAliasId? { + val value = trim() + if (value.isEmpty()) return null + + val fullAlias = when { + value.startsWith("#") && value.contains(":") -> value + value.startsWith("#") -> "$value:$defaultDomain" + value.contains(":") -> "#$value" + else -> "#$value:$defaultDomain" + } + + return RoomAliasId(fullAlias).takeIf { it.isValid } +} diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/JoinRoom.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/JoinRoom.kt new file mode 100755 index 0000000..ed11441 --- /dev/null +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/JoinRoom.kt @@ -0,0 +1,450 @@ +/* + * Created by sweetbread + * Copyright (c) 2026. All rights reserved. + */ + +package ru.risdeveau.pixeldragon.ui.layout + +import android.net.Uri +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AssistChip +import androidx.compose.material3.Button +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import de.connect2x.trixnity.clientserverapi.model.room.GetPublicRoomsResponse +import de.connect2x.trixnity.clientserverapi.model.room.GetPublicRoomsWithFilter +import de.connect2x.trixnity.core.model.RoomAliasId +import de.connect2x.trixnity.core.model.RoomId +import de.connect2x.trixnity.core.model.events.m.room.CreateEventContent +import de.connect2x.trixnity.core.model.events.m.room.JoinRulesEventContent +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import ru.risdeveau.pixeldragon.client +import ru.risdeveau.pixeldragon.ui.item.Avatar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun JoinRoomScreen( + modifier: Modifier = Modifier, + onJoined: (RoomId) -> Unit, +) { + val matrixClient = client!! + val scope = rememberCoroutineScope() + + var query by remember { mutableStateOf("") } + var server by remember { mutableStateOf(matrixClient.userId.domain) } + var results by remember { mutableStateOf>(emptyList()) } + var nextBatch by remember { mutableStateOf(null) } + var isSearching by remember { mutableStateOf(false) } + var isJoining by remember { mutableStateOf(false) } + var error by remember { mutableStateOf(null) } + + val directTarget = remember(query) { query.parseJoinTarget() } + + fun searchPublicRooms(since: String? = null) { + isSearching = true + error = null + + scope.launch { + val searchTerm = query.trim().takeUnless { directTarget != null } + val result = matrixClient.api.room.getPublicRooms( + limit = 30, + server = server.trim().ifEmpty { null }, + since = since, + filter = GetPublicRoomsWithFilter.Request.Filter( + genericSearchTerm = searchTerm?.takeIf { it.isNotBlank() }, + ), + ) + + result.fold( + onSuccess = { response -> + results = if (since == null) response.chunk else results + response.chunk + nextBatch = response.nextBatch + }, + onFailure = { throwable -> + if (since == null) results = emptyList() + nextBatch = null + error = throwable.message ?: "Could not search rooms" + }, + ) + + isSearching = false + } + } + + fun joinTarget(target: JoinTarget) { + isJoining = true + error = null + + scope.launch { + val result = when (target) { + is JoinTarget.Alias -> matrixClient.api.room.joinRoom( + roomAliasId = RoomAliasId(target.alias), + via = target.via.takeIf { it.isNotEmpty() }, + ) + is JoinTarget.Room -> matrixClient.api.room.joinRoom( + roomId = RoomId(target.roomId), + via = target.via.takeIf { it.isNotEmpty() }, + ) + } + + result.fold( + onSuccess = { roomId -> onJoined(roomId) }, + onFailure = { throwable -> + error = throwable.message ?: "Could not join room" + }, + ) + + isJoining = false + } + } + + LaunchedEffect(query, server) { + delay(450) + if (query.parseJoinTarget() == null) { + searchPublicRooms() + } else { + results = emptyList() + nextBatch = null + } + } + + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = "Join room or space", + style = MaterialTheme.typography.headlineSmall, + ) + + Text( + text = "Search a public room directory, paste #alias:server, !roomId:server or a matrix.to link.", + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = query, + onValueChange = { query = it }, + singleLine = true, + label = { Text("Search, alias or Matrix link") }, + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = server, + onValueChange = { server = it.trim() }, + singleLine = true, + label = { Text("Directory server") }, + supportingText = { Text("Leave current homeserver or enter another server, for example matrix.org") }, + ) + } + } + + directTarget?.let { target -> + item { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + ), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text( + text = "Direct join", + style = MaterialTheme.typography.titleMedium, + ) + Text( + text = target.id, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + if (target.via.isNotEmpty()) { + Text( + text = "via ${target.via.joinToString()}", + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Button( + enabled = !isJoining, + onClick = { joinTarget(target) }, + ) { + if (isJoining) { + CircularProgressIndicator() + } else { + Text("Join") + } + } + } + } + } + } + + error?.let { + item { + Text( + text = it, + color = MaterialTheme.colorScheme.error, + ) + } + } + + if (directTarget == null) { + item { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = if (query.isBlank()) "Public directory" else "Search results", + style = MaterialTheme.typography.titleMedium, + ) + + if (isSearching) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + } + } + } + + if (!isSearching && results.isEmpty()) { + item { + Text( + text = "No rooms found", + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + items( + items = results, + key = { it.roomId.full }, + ) { room -> + PublicRoomItem( + room = room, + isJoining = isJoining, + onJoin = { + joinTarget( + JoinTarget.Room( + roomId = room.roomId.full, + via = room.canonicalAlias?.domain?.let { setOf(it) }.orEmpty(), + ), + ) + }, + onKnock = { + isJoining = true + error = null + scope.launch { + val result = matrixClient.api.room.knockRoom(room.roomId) + result.fold( + onSuccess = { roomId -> onJoined(roomId) }, + onFailure = { throwable -> error = throwable.message ?: "Could not knock room" }, + ) + isJoining = false + } + }, + ) + } + + nextBatch?.let { since -> + item { + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + enabled = !isSearching, + onClick = { searchPublicRooms(since) }, + ) { + Text("Load more") + } + } + } + } + } +} + +@Composable +private fun PublicRoomItem( + room: GetPublicRoomsResponse.PublicRoomsChunk, + isJoining: Boolean, + onJoin: () -> Unit, + onKnock: () -> Unit, +) { + val isSpace = room.roomType == CreateEventContent.RoomType.Space + val title = room.name ?: room.canonicalAlias?.full ?: room.roomId.full + val avatarShape = if (isSpace) RoundedCornerShape(12.dp) else CircleShape + val canKnock = room.joinRule == JoinRulesEventContent.JoinRule.Knock || + room.joinRule == JoinRulesEventContent.JoinRule.KnockRestricted + + ElevatedCard(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Avatar( + modifier = Modifier + .size(48.dp) + .clip(avatarShape), + url = room.avatarUrl, + fallbackName = title, + ) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleMedium, + ) + room.canonicalAlias?.let { + Text( + text = it.full, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + + room.topic?.takeIf { it.isNotBlank() }?.let { + Text( + text = it, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + AssistChip( + onClick = {}, + label = { Text(if (isSpace) "space" else "room") }, + ) + AssistChip( + onClick = {}, + label = { Text(room.joinRule.name) }, + ) + AssistChip( + onClick = {}, + label = { Text("${room.joinedMembersCount} members") }, + ) + } + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Button( + enabled = !isJoining, + onClick = onJoin, + ) { + Text("Join") + } + + if (canKnock) { + OutlinedButton( + enabled = !isJoining, + onClick = onKnock, + ) { + Text("Knock") + } + } + } + } + } +} + +private sealed interface JoinTarget { + val id: String + val via: Set + + data class Alias( + val alias: String, + override val via: Set = emptySet(), + ) : JoinTarget { + override val id: String = alias + } + + data class Room( + val roomId: String, + override val via: Set = emptySet(), + ) : JoinTarget { + override val id: String = roomId + } +} + +private fun String.parseJoinTarget(): JoinTarget? { + val value = trim() + if (value.isEmpty()) return null + + if (value.startsWith("https://matrix.to/#/") || value.startsWith("http://matrix.to/#/")) { + return value.parseMatrixToTarget() + } + + if (value.startsWith("#") && RoomAliasId(value).isValid) { + return JoinTarget.Alias(value) + } + + if (value.startsWith("!") && RoomId(value).isReasonable) { + return JoinTarget.Room(value) + } + + return null +} + +private fun String.parseMatrixToTarget(): JoinTarget? { + val fragment = Uri.parse(this).fragment.orEmpty().removePrefix("/") + if (fragment.isBlank()) return null + + val idPart = fragment.substringBefore('?') + val queryPart = fragment.substringAfter('?', missingDelimiterValue = "") + val id = Uri.decode(idPart) + val via = Uri.parse("https://matrix.to/?$queryPart") + .getQueryParameters("via") + .filter { it.isNotBlank() } + .toSet() + + return when { + id.startsWith("#") && RoomAliasId(id).isValid -> JoinTarget.Alias(id, via) + id.startsWith("!") && RoomId(id).isReasonable -> JoinTarget.Room(id, via) + else -> null + } +} diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/RoomActions.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/RoomActions.kt new file mode 100755 index 0000000..05f6dbb --- /dev/null +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/RoomActions.kt @@ -0,0 +1,110 @@ +/* + * Created by sweetbread + * Copyright (c) 2026. All rights reserved. + */ + +package ru.risdeveau.pixeldragon.ui.layout + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +object RoomActionsRoutes { + const val Rooms = "rooms" + const val JoinRoom = "join-room" + const val CreateRoom = "create-room" + const val CreateSpace = "create-space" + + fun titleFor(route: String?): String? { + return when (route) { + JoinRoom -> "Join room or space" + CreateRoom -> "Create room" + CreateSpace -> "Create space" + else -> null + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomActionsSheet( + onDismiss: () -> Unit, + onJoinRoom: () -> Unit, + onCreateRoom: () -> Unit, + onCreateSpace: () -> Unit, +) { + ModalBottomSheet(onDismissRequest = onDismiss) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + ) { + Column { + RoomActionItem( + title = "Join room or space", + subtitle = "Search public directories or paste a Matrix link", + onClick = { + onDismiss() + onJoinRoom() + }, + ) + + HorizontalDivider(modifier = Modifier.padding(start = 16.dp)) + + RoomActionItem( + title = "Create room", + subtitle = "Start a public or private room", + onClick = { + onDismiss() + onCreateRoom() + }, + ) + + HorizontalDivider(modifier = Modifier.padding(start = 16.dp)) + + RoomActionItem( + title = "Create space", + subtitle = "Group related rooms together", + onClick = { + onDismiss() + onCreateSpace() + }, + ) + } + } + + Spacer(Modifier.height(32.dp)) + } + } +} + +@Composable +private fun RoomActionItem( + title: String, + subtitle: String, + onClick: () -> Unit, +) { + ListItem( + modifier = Modifier.clickable(onClick = onClick), + headlineContent = { Text(title) }, + supportingContent = { Text(subtitle) }, + trailingContent = { Text("›") }, + ) +}