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.
This commit is contained in:
parent
c3600690d5
commit
54a8e7e1ce
|
|
@ -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,
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
}
|
||||
|
||||
// Initialize by loading servers
|
||||
init {
|
||||
loadServers()
|
||||
|
|
@ -53,4 +74,34 @@ class DiscoverServersListViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadServerInviteInfo(inviteCode: String): Flow<RsResult<Invite, RevoltError>> = 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<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)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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<RsResult<Invite, RevoltError>?>(null) }
|
||||
var error by remember { mutableStateOf<RevoltError?>(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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>(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<ServerData>,
|
||||
val categories: List<ServerCategory>
|
||||
) : DiscoverUiState()
|
||||
|
||||
/**
|
||||
* Error state
|
||||
* @param message The error message
|
||||
*/
|
||||
data class Error(val message: String) : DiscoverUiState()
|
||||
}
|
||||
|
|
@ -547,6 +547,7 @@
|
|||
<string name="invite_join">Join</string>
|
||||
<string name="invite_cancel">Cancel</string>
|
||||
<string name="invite_already_member">You are already a member of this server.</string>
|
||||
<string name="members_count">%1$d members</string>
|
||||
<string name="invite_error_header">There was an error</string>
|
||||
<string name="invite_error_no_invite">No invite code was specified.</string>
|
||||
<string name="invite_error_invalid_invite">Could not find an invite with the specified code.</string>
|
||||
|
|
|
|||
Loading…
Reference in New Issue