From 54a8e7e1ce13fe643f310703039f3645dd1f9fda Mon Sep 17 00:00:00 2001 From: AbronStudio Date: Tue, 5 Aug 2025 12:09:06 +0330 Subject: [PATCH] feat(discover): Implement server joining functionality - Added `ServerInviteHandler.kt` and `ServerInviteDialog.kt` to manage and display server invite information and allow users to join servers. - Updated `DiscoverServersListViewModel.kt` to include logic for fetching invite details and joining servers. - Modified `DiscoverServersList.kt` to trigger the server invite dialog when a server item is clicked and to handle the invite code. - Added `inviteCode` field to `ServerData` in `ServerDataRepository.kt`. - Removed unused `DiscoverViewModel.kt`. - Added new string resources for invite dialog messages and member count. --- .../googlesheets/ServerDataRepository.kt | 2 + .../chat/discover/DiscoverServersList.kt | 25 ++- .../discover/DiscoverServersListViewModel.kt | 51 +++++ .../chat/discover/ServerInviteDialog.kt | 183 ++++++++++++++++++ .../chat/discover/ServerInviteHandler.kt | 69 +++++++ .../screens/discover/DiscoverViewModel.kt | 88 --------- app/src/main/res/values/strings.xml | 1 + 7 files changed, 330 insertions(+), 89 deletions(-) create mode 100644 app/src/main/java/chat/revolt/composables/screens/chat/discover/ServerInviteDialog.kt create mode 100644 app/src/main/java/chat/revolt/composables/screens/chat/discover/ServerInviteHandler.kt delete mode 100644 app/src/main/java/chat/revolt/screens/discover/DiscoverViewModel.kt diff --git a/app/src/main/java/chat/revolt/api/routes/googlesheets/ServerDataRepository.kt b/app/src/main/java/chat/revolt/api/routes/googlesheets/ServerDataRepository.kt index 41f42659..88b939c7 100644 --- a/app/src/main/java/chat/revolt/api/routes/googlesheets/ServerDataRepository.kt +++ b/app/src/main/java/chat/revolt/api/routes/googlesheets/ServerDataRepository.kt @@ -24,6 +24,7 @@ class ServerDataRepository { id = rowData["id"] ?: "", name = rowData["name"] ?: "", description = rowData["description"] ?: "", + inviteCode = rowData["inviteCode"] ?: "", ) } emit(servers) @@ -58,6 +59,7 @@ data class ServerData( val id: String, val name: String, val description: String, + val inviteCode: String, ) /** diff --git a/app/src/main/java/chat/revolt/composables/screens/chat/discover/DiscoverServersList.kt b/app/src/main/java/chat/revolt/composables/screens/chat/discover/DiscoverServersList.kt index 1d483bed..de0435ba 100644 --- a/app/src/main/java/chat/revolt/composables/screens/chat/discover/DiscoverServersList.kt +++ b/app/src/main/java/chat/revolt/composables/screens/chat/discover/DiscoverServersList.kt @@ -21,6 +21,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -39,6 +42,22 @@ fun DiscoverServersList() { val servers by viewModel.servers.collectAsState() val isLoading by viewModel.isLoading.collectAsState() val error by viewModel.error.collectAsState() + val selectedInviteCode by viewModel.selectedServerInviteCodeFlow.collectAsState() + + // Handle server invite dialog + selectedInviteCode?.let { inviteCode -> + ServerInviteHandler( + inviteCode = inviteCode, + viewModel = viewModel, + onDismiss = { + viewModel.selectedServerInviteCode = null + }, + onJoinSuccess = { + // Show a success message or navigate to the joined server + viewModel.selectedServerInviteCode = null + } + ) + } Column( modifier = Modifier @@ -90,7 +109,11 @@ fun DiscoverServersList() { items(servers) { server -> ServerItem( server = server, - onClick = { /* Handle server selection */ } + onClick = { + if (server.inviteCode.isNotEmpty()) { + viewModel.selectedServerInviteCode = server.inviteCode + } + } ) } item { diff --git a/app/src/main/java/chat/revolt/composables/screens/chat/discover/DiscoverServersListViewModel.kt b/app/src/main/java/chat/revolt/composables/screens/chat/discover/DiscoverServersListViewModel.kt index fbb5cce3..e2f79d3d 100644 --- a/app/src/main/java/chat/revolt/composables/screens/chat/discover/DiscoverServersListViewModel.kt +++ b/app/src/main/java/chat/revolt/composables/screens/chat/discover/DiscoverServersListViewModel.kt @@ -2,12 +2,22 @@ package chat.revolt.composables.screens.chat.discover import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import chat.revolt.api.RevoltError import chat.revolt.api.routes.googlesheets.ServerData import chat.revolt.api.routes.googlesheets.ServerDataRepository +import chat.revolt.api.routes.invites.fetchInviteByCode +import chat.revolt.api.routes.invites.joinInviteByCode +import chat.revolt.api.schemas.Invite +import chat.revolt.api.schemas.InviteJoined +import chat.revolt.api.schemas.RsResult import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import javax.inject.Inject @@ -28,6 +38,17 @@ class DiscoverServersListViewModel @Inject constructor( private val _error = MutableStateFlow(null) val error: StateFlow = _error.asStateFlow() + // Selected server invite code + private val _selectedServerInviteCode = MutableStateFlow(null) + val selectedServerInviteCodeFlow: StateFlow = _selectedServerInviteCode.asStateFlow() + + // Property for easy access to selectedServerInviteCode + var selectedServerInviteCode: String? + get() = _selectedServerInviteCode.value + set(value) { + _selectedServerInviteCode.value = value + } + // Initialize by loading servers init { loadServers() @@ -53,4 +74,34 @@ class DiscoverServersListViewModel @Inject constructor( } } } + + fun loadServerInviteInfo(inviteCode: String): Flow> = flow { + _isLoading.value = true + _error.value = null + + try { + val result = fetchInviteByCode(inviteCode) + emit(result) + } catch (e: Exception) { + _error.value = e.message ?: "Unknown error occurred" + emit(RsResult.err(RevoltError("Unknown"))) + } finally { + _isLoading.value = false + } + }.flowOn(Dispatchers.IO) + + fun joinServer(inviteCode: String): Flow> = flow { + _isLoading.value = true + _error.value = null + + try { + val result = joinInviteByCode(inviteCode) + emit(result) + } catch (e: Exception) { + _error.value = e.message ?: "Unknown error occurred" + emit(RsResult.err(RevoltError("Unknown"))) + } finally { + _isLoading.value = false + } + }.flowOn(Dispatchers.IO) } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/composables/screens/chat/discover/ServerInviteDialog.kt b/app/src/main/java/chat/revolt/composables/screens/chat/discover/ServerInviteDialog.kt new file mode 100644 index 00000000..20a9edd1 --- /dev/null +++ b/app/src/main/java/chat/revolt/composables/screens/chat/discover/ServerInviteDialog.kt @@ -0,0 +1,183 @@ +package chat.revolt.composables.screens.chat.discover + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.revolt.R +import chat.revolt.api.REVOLT_FILES +import chat.revolt.api.RevoltError +import chat.revolt.api.schemas.Invite +import chat.revolt.composables.generic.IconPlaceholder +import chat.revolt.composables.generic.RemoteImage + +@Composable +fun ServerInviteDialog( + isLoading: Boolean, + invite: Invite?, + error: RevoltError?, + onJoinClick: () -> Unit, + onDismiss: () -> Unit +) { + if (isLoading) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = stringResource(id = R.string.loading), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + }, + text = { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + ) { + CircularProgressIndicator( + modifier = Modifier.size(48.dp) + ) + } + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(id = R.string.invite_cancel)) + } + } + ) + return + } + + if (error != null) { + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + painter = painterResource(R.drawable.icn_error_24dp), + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + }, + title = { + Text( + text = stringResource(id = R.string.invite_error_header), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + }, + text = { + Text( + text = when (error.type) { + "NotFound" -> stringResource(id = R.string.invite_error_invalid_invite) + "Banned" -> stringResource(id = R.string.invite_error_banned) + else -> stringResource(id = R.string.invite_error_unknown) + }, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(id = R.string.invite_cancel)) + } + } + ) + return + } + + if (invite == null) { + // This shouldn't happen, but handle it just in case + return + } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = invite.serverName ?: stringResource(id = R.string.unknown), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + modifier = Modifier.fillMaxWidth() + ) + }, + text = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + if (invite.serverIcon != null) { + RemoteImage( + url = "$REVOLT_FILES/icons/${invite.serverIcon.id}/${invite.serverIcon.filename}", + allowAnimation = false, + description = invite.serverName ?: stringResource(id = R.string.unknown), + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + ) + } else { + IconPlaceholder( + name = invite.serverName ?: stringResource(id = R.string.unknown), + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(id = R.string.invite_message), + textAlign = TextAlign.Center + ) + + if (invite.memberCount != null) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource( + id = R.string.members_count, + invite.memberCount + ), + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + }, + confirmButton = { + Button(onClick = onJoinClick) { + Text(text = stringResource(id = R.string.invite_join)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(id = R.string.invite_cancel)) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/composables/screens/chat/discover/ServerInviteHandler.kt b/app/src/main/java/chat/revolt/composables/screens/chat/discover/ServerInviteHandler.kt new file mode 100644 index 00000000..8956ca49 --- /dev/null +++ b/app/src/main/java/chat/revolt/composables/screens/chat/discover/ServerInviteHandler.kt @@ -0,0 +1,69 @@ +package chat.revolt.composables.screens.chat.discover + +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.platform.LocalContext +import androidx.lifecycle.viewmodel.compose.viewModel +import chat.revolt.api.RevoltError +import chat.revolt.api.schemas.Invite +import chat.revolt.api.schemas.RsResult +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@Composable +fun ServerInviteHandler( + inviteCode: String, + viewModel: DiscoverServersListViewModel = viewModel(), + onDismiss: () -> Unit = {}, + onJoinSuccess: () -> Unit = {} +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + var isLoading by remember { mutableStateOf(true) } + var showDialog by remember { mutableStateOf(true) } + var inviteResult by remember { mutableStateOf?>(null) } + var error by remember { mutableStateOf(null) } + + // Fetch server invite info + LaunchedEffect(inviteCode) { + viewModel.loadServerInviteInfo(inviteCode).collectLatest { result -> + inviteResult = result + isLoading = false + + if (result.err) { + error = result.error + } + } + } + + if (showDialog) { + ServerInviteDialog( + isLoading = isLoading, + invite = inviteResult?.value, + error = error, + onJoinClick = { + isLoading = true + scope.launch { + viewModel.joinServer(inviteCode).collectLatest { result -> + isLoading = false + if (result.ok) { + showDialog = false + onJoinSuccess() + } else { + error = result.error + } + } + } + }, + onDismiss = { + showDialog = false + onDismiss() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/discover/DiscoverViewModel.kt b/app/src/main/java/chat/revolt/screens/discover/DiscoverViewModel.kt deleted file mode 100644 index eb86575e..00000000 --- a/app/src/main/java/chat/revolt/screens/discover/DiscoverViewModel.kt +++ /dev/null @@ -1,88 +0,0 @@ -package chat.revolt.screens.discover - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import chat.revolt.api.routes.googlesheets.ServerCategory -import chat.revolt.api.routes.googlesheets.ServerData -import chat.revolt.api.routes.googlesheets.ServerDataRepository -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.launch -import javax.inject.Inject - -/** - * ViewModel for the Discover screen - * Handles fetching and managing server data from Google Sheets - */ -@HiltViewModel -class DiscoverViewModel @Inject constructor( - private val serverDataRepository: ServerDataRepository -) : ViewModel() { - - // UI state - var uiState by mutableStateOf(DiscoverUiState.Loading) - private set - - // Initialize the ViewModel - init { - loadServerData() - } - - /** - * Loads server data from the repository - */ - fun loadServerData() { - uiState = DiscoverUiState.Loading - viewModelScope.launch { - serverDataRepository.getServers( - sheetUrl = "https://docs.google.com/spreadsheets/d/e/2PACX-1vRY41D-NgTE6bC3kTN3dRpisI-DoeHG8Eg7n31xb1CdydWjOLaphqYckkTiaG9oIQSWP92h3NE-7cpF/pub?gid=0&single=true&output=csv" - ) - .catch { exception -> - uiState = DiscoverUiState.Error(exception.message ?: "Unknown error") - } - .collect { servers -> - serverDataRepository.getServerCategories( - sheetUrl = "https://docs.google.com/spreadsheets/d/e/2PACX-1vRY41D-NgTE6bC3kTN3dRpisI-DoeHG8Eg7n31xb1CdydWjOLaphqYckkTiaG9oIQSWP92h3NE-7cpF/pub?gid=0&single=true&output=csv" - ) - .catch { exception -> - uiState = DiscoverUiState.Error(exception.message ?: "Unknown error") - } - .collect { categories -> - uiState = DiscoverUiState.Success( - servers = servers, - categories = categories - ) - } - } - } - } -} - -/** - * Sealed class representing the UI state of the Discover screen - */ -sealed class DiscoverUiState { - /** - * Loading state - */ - object Loading : DiscoverUiState() - - /** - * Success state with data - * @param servers The list of servers - * @param categories The list of server categories - */ - data class Success( - val servers: List, - val categories: List - ) : DiscoverUiState() - - /** - * Error state - * @param message The error message - */ - data class Error(val message: String) : DiscoverUiState() -} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fbb982b9..4c70cf27 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -547,6 +547,7 @@ Join Cancel You are already a member of this server. + %1$d members There was an error No invite code was specified. Could not find an invite with the specified code.