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 10b3a120..bc1b66a3 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 @@ -38,27 +38,23 @@ import chat.revolt.composables.screens.chat.discover.ServerInviteHandler @Composable fun DiscoverServersList() { val viewModel = hiltViewModel() - val servers by viewModel.servers.collectAsState() - val isLoading by viewModel.isLoading.collectAsState() - val error by viewModel.error.collectAsState() - val selectedInviteCode by viewModel.selectedServerInviteCodeFlow.collectAsState() - val processingServerId by viewModel.processingServerId.collectAsState() + val uiState by viewModel.uiState.collectAsState() // Handle server invite dialog - selectedInviteCode?.let { inviteCode -> + uiState.selectedInviteCode?.let { inviteCode -> // Find the server with this invite code - val selectedServer = servers.find { it.inviteCode == inviteCode } + val selectedServer = uiState.servers.find { it.inviteCode == inviteCode } selectedServer?.let { server -> ServerInviteHandler( inviteCode = inviteCode, serverId = server.id, viewModel = viewModel, onDismiss = { - viewModel.selectedServerInviteCode = null + viewModel.setSelectedInviteCode(null) }, onJoinSuccess = { // Show a success message or navigate to the joined server - viewModel.selectedServerInviteCode = null + viewModel.setSelectedInviteCode(null) } ) } @@ -92,17 +88,17 @@ fun DiscoverServersList() { Spacer(modifier = Modifier.height(16.dp)) when { - isLoading -> { + uiState.isLoading -> { CircularProgressIndicator( modifier = Modifier .padding(16.dp) .align(Alignment.CenterHorizontally) ) } - error != null -> { + uiState.error != null -> { Text(stringResource(R.string.error)) } - servers.isEmpty() -> { + uiState.servers.isEmpty() -> { Text(stringResource(R.string.no_servers_found)) } else -> { @@ -111,10 +107,10 @@ fun DiscoverServersList() { .fillMaxSize() .weight(1f) ) { - items(servers) { server -> + items(uiState.servers) { server -> ServerItem( server = server, - isProcessing = processingServerId == server.id, + isProcessing = uiState.processingServerId == server.id, onClick = { if (server.inviteCode.isNotEmpty()) { // First load server data, dialog will be shown after data is loaded 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 83ed6137..ae55a2ed 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 @@ -21,154 +21,137 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import javax.inject.Inject +/** + * Combined UI state for the discover servers screen + */ +data class DiscoverUiState( + // Server list state + val servers: List = emptyList(), + val isLoading: Boolean = false, + val error: String? = null, + + // Server invite state + val selectedInviteCode: String? = null, + val processingServerId: String? = null, + val loadedInviteData: Invite? = null, + val loadedError: RevoltError? = null +) + @HiltViewModel class DiscoverServersListViewModel @Inject constructor( private val serverDataRepository: ServerDataRepository, ) : ViewModel() { - // Use StateFlow to properly expose the server list to the UI - private val _servers = MutableStateFlow>(emptyList()) - val servers: StateFlow> = _servers.asStateFlow() + // Single UI state + private val _uiState = MutableStateFlow(DiscoverUiState()) + val uiState: StateFlow = _uiState.asStateFlow() - // Loading state - private val _isLoading = MutableStateFlow(false) - val isLoading: StateFlow = _isLoading.asStateFlow() - - // Error state - 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 - } - - // Currently processing server ID - private val _processingServerId = MutableStateFlow(null) - val processingServerId: StateFlow = _processingServerId.asStateFlow() - - // Server invite data that has been loaded - private val _loadedInviteData = MutableStateFlow(null) - val loadedInviteData: StateFlow = _loadedInviteData.asStateFlow() - - // Error that occurred during loading server data - private val _loadedError = MutableStateFlow(null) - val loadedError: StateFlow = _loadedError.asStateFlow() - - // Initialize by loading servers + // Initialize init { loadServers() } + /** + * Load server list from repository + */ fun loadServers() { - _isLoading.value = true - _error.value = null + updateState { it.copy(isLoading = true, error = null) } viewModelScope.launch { try { - // Use the Google Sheets published CSV URL since that's what you were using before val csvUrl = "https://docs.google.com/spreadsheets/d/e/2PACX-1vRY41D-NgTE6bC3kTN3dRpisI-DoeHG8Eg7n31xb1CdydWjOLaphqYckkTiaG9oIQSWP92h3NE-7cpF/pub?gid=0&single=true&output=csv" - - serverDataRepository.getServers(csvUrl) - .collect { serverList -> - _servers.value = serverList - _isLoading.value = false - } + serverDataRepository.getServers(csvUrl).collect { servers -> + updateState { it.copy(servers = servers, isLoading = false) } + } } catch (e: Exception) { - _error.value = e.message ?: "Unknown error occurred" - _isLoading.value = false + updateState { + it.copy( + error = e.message ?: "Unknown error occurred", + isLoading = false + ) + } } } } - fun loadServerInviteInfo(inviteCode: String, serverId: String): Flow> = flow { - _processingServerId.value = serverId - _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 { - _processingServerId.value = null - } - }.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) - /** - * Join server without showing loading in the server list - * This is used when joining from the dialog where we only want the button to show loading - */ - fun joinServerWithoutProcessingIndicator(inviteCode: String): Flow> = flow { - // Don't set _processingServerId or _isLoading here - _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"))) - } - }.flowOn(Dispatchers.IO) - - /** - * Load server data first, then show the dialog with the loaded data + * Load server data and show dialog when ready */ fun loadServerDataAndShowDialog(inviteCode: String, serverId: String) { - _loadedInviteData.value = null - _processingServerId.value = serverId - _error.value = null + updateState { + it.copy( + loadedInviteData = null, + processingServerId = serverId + ) + } viewModelScope.launch { try { val result = fetchInviteByCode(inviteCode) if (result.ok) { - _loadedInviteData.value = result.value + updateState { + it.copy( + loadedInviteData = result.value, + selectedInviteCode = inviteCode, + processingServerId = null + ) + } } else { - // Store the error in loadedError - _loadedError.value = result.error + updateState { + it.copy( + loadedError = result.error, + selectedInviteCode = inviteCode, + processingServerId = null + ) + } + } + } catch (_: Exception) { + updateState { + it.copy( + loadedError = RevoltError("Unknown"), + selectedInviteCode = inviteCode, + processingServerId = null + ) } - // Set the selected invite code to show the dialog in both success and error cases - _selectedServerInviteCode.value = inviteCode - } catch (e: Exception) { - // Handle exception by creating a generic error - _loadedError.value = RevoltError("Unknown") - _selectedServerInviteCode.value = inviteCode - } finally { - _processingServerId.value = null } } } + /** + * Join server without showing loading in the server list + */ + fun joinServerWithoutProcessingIndicator(inviteCode: String): Flow> = flow { + try { + emit(joinInviteByCode(inviteCode)) + } catch (_: Exception) { + emit(RsResult.err(RevoltError("Unknown"))) + } + }.flowOn(Dispatchers.IO) + /** * Clear loaded data when dialog is dismissed */ fun clearLoadedData() { - _loadedInviteData.value = null - _loadedError.value = null - _selectedServerInviteCode.value = null + updateState { + it.copy( + loadedInviteData = null, + loadedError = null, + selectedInviteCode = null + ) + } } -} \ No newline at end of file + + /** + * Set selected invite code + */ + fun setSelectedInviteCode(code: String?) { + updateState { it.copy(selectedInviteCode = code) } + } + + /** + * Helper function to update state + */ + private fun updateState(update: (DiscoverUiState) -> DiscoverUiState) { + _uiState.value = update(_uiState.value) + } +} \ 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 index 89edf669..962e66dd 100644 --- 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 @@ -29,15 +29,14 @@ fun ServerInviteHandler( var showDialog by remember { mutableStateOf(true) } var error by remember { mutableStateOf(null) } - // Get the pre-loaded invite data and error - val loadedInviteData by viewModel.loadedInviteData.collectAsState() - val loadedError by viewModel.loadedError.collectAsState() + // Get the UI state + val uiState by viewModel.uiState.collectAsState() if (showDialog) { ServerInviteDialog( isLoading = isJoining, - invite = loadedInviteData, - error = error ?: loadedError, + invite = uiState.loadedInviteData, + error = error ?: uiState.loadedError, onJoinClick = { isJoining = true scope.launch {