refactor(discover): Consolidate UI state management in DiscoverServersList

- Introduced a single `DiscoverUiState` data class to encapsulate all UI-related states for the discover servers screen.
- Refactored `DiscoverServersListViewModel` to utilize the new UI state, simplifying state management and improving readability.
- Updated `DiscoverServersList` to collect and use the consolidated UI state, enhancing the handling of loading, error, and server data.
- Modified `ServerInviteHandler` to leverage the new UI state for better error and invite data management.
- Improved code cleanliness by removing redundant state flows and variables.
This commit is contained in:
AbronStudio 2025-08-05 16:53:41 +03:30
parent 8de2f2d8bd
commit 39a948be1f
3 changed files with 109 additions and 131 deletions

View File

@ -38,27 +38,23 @@ import chat.revolt.composables.screens.chat.discover.ServerInviteHandler
@Composable @Composable
fun DiscoverServersList() { fun DiscoverServersList() {
val viewModel = hiltViewModel<DiscoverServersListViewModel>() val viewModel = hiltViewModel<DiscoverServersListViewModel>()
val servers by viewModel.servers.collectAsState() val uiState by viewModel.uiState.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()
// Handle server invite dialog // Handle server invite dialog
selectedInviteCode?.let { inviteCode -> uiState.selectedInviteCode?.let { inviteCode ->
// Find the server with this invite code // 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 -> selectedServer?.let { server ->
ServerInviteHandler( ServerInviteHandler(
inviteCode = inviteCode, inviteCode = inviteCode,
serverId = server.id, serverId = server.id,
viewModel = viewModel, viewModel = viewModel,
onDismiss = { onDismiss = {
viewModel.selectedServerInviteCode = null viewModel.setSelectedInviteCode(null)
}, },
onJoinSuccess = { onJoinSuccess = {
// Show a success message or navigate to the joined server // 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)) Spacer(modifier = Modifier.height(16.dp))
when { when {
isLoading -> { uiState.isLoading -> {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier modifier = Modifier
.padding(16.dp) .padding(16.dp)
.align(Alignment.CenterHorizontally) .align(Alignment.CenterHorizontally)
) )
} }
error != null -> { uiState.error != null -> {
Text(stringResource(R.string.error)) Text(stringResource(R.string.error))
} }
servers.isEmpty() -> { uiState.servers.isEmpty() -> {
Text(stringResource(R.string.no_servers_found)) Text(stringResource(R.string.no_servers_found))
} }
else -> { else -> {
@ -111,10 +107,10 @@ fun DiscoverServersList() {
.fillMaxSize() .fillMaxSize()
.weight(1f) .weight(1f)
) { ) {
items(servers) { server -> items(uiState.servers) { server ->
ServerItem( ServerItem(
server = server, server = server,
isProcessing = processingServerId == server.id, isProcessing = uiState.processingServerId == server.id,
onClick = { onClick = {
if (server.inviteCode.isNotEmpty()) { if (server.inviteCode.isNotEmpty()) {
// First load server data, dialog will be shown after data is loaded // First load server data, dialog will be shown after data is loaded

View File

@ -21,154 +21,137 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
/**
* Combined UI state for the discover servers screen
*/
data class DiscoverUiState(
// Server list state
val servers: List<ServerData> = 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 @HiltViewModel
class DiscoverServersListViewModel @Inject constructor( class DiscoverServersListViewModel @Inject constructor(
private val serverDataRepository: ServerDataRepository, private val serverDataRepository: ServerDataRepository,
) : ViewModel() { ) : ViewModel() {
// Use StateFlow to properly expose the server list to the UI // Single UI state
private val _servers = MutableStateFlow<List<ServerData>>(emptyList()) private val _uiState = MutableStateFlow(DiscoverUiState())
val servers: StateFlow<List<ServerData>> = _servers.asStateFlow() val uiState: StateFlow<DiscoverUiState> = _uiState.asStateFlow()
// Loading state // Initialize
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
// Error state
private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error.asStateFlow()
// Selected server invite code
private val _selectedServerInviteCode = MutableStateFlow<String?>(null)
val selectedServerInviteCodeFlow: StateFlow<String?> = _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<String?>(null)
val processingServerId: StateFlow<String?> = _processingServerId.asStateFlow()
// Server invite data that has been loaded
private val _loadedInviteData = MutableStateFlow<Invite?>(null)
val loadedInviteData: StateFlow<Invite?> = _loadedInviteData.asStateFlow()
// Error that occurred during loading server data
private val _loadedError = MutableStateFlow<RevoltError?>(null)
val loadedError: StateFlow<RevoltError?> = _loadedError.asStateFlow()
// Initialize by loading servers
init { init {
loadServers() loadServers()
} }
/**
* Load server list from repository
*/
fun loadServers() { fun loadServers() {
_isLoading.value = true updateState { it.copy(isLoading = true, error = null) }
_error.value = null
viewModelScope.launch { viewModelScope.launch {
try { 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" 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 { servers ->
serverDataRepository.getServers(csvUrl) updateState { it.copy(servers = servers, isLoading = false) }
.collect { serverList -> }
_servers.value = serverList
_isLoading.value = false
}
} catch (e: Exception) { } catch (e: Exception) {
_error.value = e.message ?: "Unknown error occurred" updateState {
_isLoading.value = false it.copy(
error = e.message ?: "Unknown error occurred",
isLoading = false
)
}
} }
} }
} }
fun loadServerInviteInfo(inviteCode: String, serverId: String): Flow<RsResult<Invite, RevoltError>> = 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<RsResult<InviteJoined, RevoltError>> = 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 * Load server data and show dialog when ready
* This is used when joining from the dialog where we only want the button to show loading
*/
fun joinServerWithoutProcessingIndicator(inviteCode: String): Flow<RsResult<InviteJoined, RevoltError>> = 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
*/ */
fun loadServerDataAndShowDialog(inviteCode: String, serverId: String) { fun loadServerDataAndShowDialog(inviteCode: String, serverId: String) {
_loadedInviteData.value = null updateState {
_processingServerId.value = serverId it.copy(
_error.value = null loadedInviteData = null,
processingServerId = serverId
)
}
viewModelScope.launch { viewModelScope.launch {
try { try {
val result = fetchInviteByCode(inviteCode) val result = fetchInviteByCode(inviteCode)
if (result.ok) { if (result.ok) {
_loadedInviteData.value = result.value updateState {
it.copy(
loadedInviteData = result.value,
selectedInviteCode = inviteCode,
processingServerId = null
)
}
} else { } else {
// Store the error in loadedError updateState {
_loadedError.value = result.error 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<RsResult<InviteJoined, RevoltError>> = flow {
try {
emit(joinInviteByCode(inviteCode))
} catch (_: Exception) {
emit(RsResult.err(RevoltError("Unknown")))
}
}.flowOn(Dispatchers.IO)
/** /**
* Clear loaded data when dialog is dismissed * Clear loaded data when dialog is dismissed
*/ */
fun clearLoadedData() { fun clearLoadedData() {
_loadedInviteData.value = null updateState {
_loadedError.value = null it.copy(
_selectedServerInviteCode.value = null loadedInviteData = null,
loadedError = null,
selectedInviteCode = null
)
}
} }
}
/**
* 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)
}
}

View File

@ -29,15 +29,14 @@ fun ServerInviteHandler(
var showDialog by remember { mutableStateOf(true) } var showDialog by remember { mutableStateOf(true) }
var error by remember { mutableStateOf<RevoltError?>(null) } var error by remember { mutableStateOf<RevoltError?>(null) }
// Get the pre-loaded invite data and error // Get the UI state
val loadedInviteData by viewModel.loadedInviteData.collectAsState() val uiState by viewModel.uiState.collectAsState()
val loadedError by viewModel.loadedError.collectAsState()
if (showDialog) { if (showDialog) {
ServerInviteDialog( ServerInviteDialog(
isLoading = isJoining, isLoading = isJoining,
invite = loadedInviteData, invite = uiState.loadedInviteData,
error = error ?: loadedError, error = error ?: uiState.loadedError,
onJoinClick = { onJoinClick = {
isJoining = true isJoining = true
scope.launch { scope.launch {