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
fun DiscoverServersList() {
val viewModel = hiltViewModel<DiscoverServersListViewModel>()
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

View File

@ -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<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
class DiscoverServersListViewModel @Inject constructor(
private val serverDataRepository: ServerDataRepository,
) : ViewModel() {
// Use StateFlow to properly expose the server list to the UI
private val _servers = MutableStateFlow<List<ServerData>>(emptyList())
val servers: StateFlow<List<ServerData>> = _servers.asStateFlow()
// Single UI state
private val _uiState = MutableStateFlow(DiscoverUiState())
val uiState: StateFlow<DiscoverUiState> = _uiState.asStateFlow()
// Loading state
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
// 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<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
* 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
* 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<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
*/
fun clearLoadedData() {
_loadedInviteData.value = null
_loadedError.value = null
_selectedServerInviteCode.value = null
updateState {
it.copy(
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 error by remember { mutableStateOf<RevoltError?>(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 {