Refactor: Update server invite handling and API URLs

- Changed the base URL for the PEP platform in `RevoltAPI.kt` to `https://pepchat.io/api`.
- Updated the `DiscoverServersList` to handle server invites more effectively by passing the server ID to the `ServerInviteHandler`.
- Enhanced `ServerInviteHandler` to utilize pre-loaded invite data and manage loading states.
- Modified `ServerInviteDialog` to improve UI elements and added previews for different states.
- Updated string resources for invite dialog messages to enhance clarity and user experience.
This commit is contained in:
AbronStudio 2025-08-05 13:19:25 +03:30
parent 330e92abdc
commit 4ddcfd8fa9
6 changed files with 254 additions and 82 deletions

View File

@ -91,14 +91,14 @@ private val PLATFORM_URLS = mapOf(
kjbook = "https://revoltchat.github.io/android"
),
ApplicationPlatform.PEP to PlatformUrls(
base = "https://a-pep.peptide.chat/api",
base = "https://pepchat.io/api",
marketing = "https://peptide.chat",
files = "https://cdn.pepusercontent.com",
january = "https://a-pep.peptide.chat/january",
january = "https://pepchat.io/january",
app = "https://peptide.chat",
invites = "https://pep.gg",
websocket = "wss://a-pep.peptide.chat/ws",
autumn = "https://a-pep.peptide.chat/autumn",
websocket = "wss://pepchat.io/ws",
autumn = "https://pepchat.io/autumn",
// TODO: Replace with correct URL
kjbook = "https://revoltchat.github.io/android"
// TODO: Replace with correct URL
@ -220,7 +220,7 @@ object RevoltAPI {
* The currently selected platform.
* Default is REVOLT.
*/
var selectedApplicationPlatform: ApplicationPlatform = ApplicationPlatform.REVOLT
var selectedApplicationPlatform: ApplicationPlatform = ApplicationPlatform.PEP
private set
/**
@ -258,7 +258,7 @@ object RevoltAPI {
// URL getter functions
fun getCurrentBaseUrl(): String = getUrlForPlatform(selectedApplicationPlatform) { it.base }
fun getCurrentMarketingUrl(): String = getUrlForPlatform(selectedApplicationPlatform) { it.marketing }
fun getCurrentFilesUrl(): String = getUrlForPlatform(selectedApplicationPlatform) { it.files }
fun getCurrentFilesUrl(): String = getUrlForPlatform(selectedApplicationPlatform) { it.autumn }
fun getCurrentJanuaryUrl(): String = getUrlForPlatform(selectedApplicationPlatform) { it.january }
fun getCurrentAppUrl(): String = getUrlForPlatform(selectedApplicationPlatform) { it.app }
fun getCurrentInvitesUrl(): String = getUrlForPlatform(selectedApplicationPlatform) { it.invites }

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@ -32,6 +33,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import chat.revolt.R
import chat.revolt.api.routes.googlesheets.ServerData
import chat.revolt.composables.screens.chat.discover.ServerInviteHandler
@Composable
fun DiscoverServersList() {
@ -40,20 +42,26 @@ fun DiscoverServersList() {
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
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
}
)
// Find the server with this invite code
val selectedServer = servers.find { it.inviteCode == inviteCode }
selectedServer?.let { server ->
ServerInviteHandler(
inviteCode = inviteCode,
serverId = server.id,
viewModel = viewModel,
onDismiss = {
viewModel.selectedServerInviteCode = null
},
onJoinSuccess = {
// Show a success message or navigate to the joined server
viewModel.selectedServerInviteCode = null
}
)
}
}
Column(
@ -106,9 +114,11 @@ fun DiscoverServersList() {
items(servers) { server ->
ServerItem(
server = server,
isProcessing = processingServerId == server.id,
onClick = {
if (server.inviteCode.isNotEmpty()) {
viewModel.selectedServerInviteCode = server.inviteCode
// First load server data, dialog will be shown after data is loaded
viewModel.loadServerDataAndShowDialog(server.inviteCode, server.id)
}
}
)
@ -125,13 +135,14 @@ fun DiscoverServersList() {
@Composable
fun ServerItem(
server: ServerData,
onClick: () -> Unit
onClick: () -> Unit,
isProcessing: Boolean = false
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.clickable { onClick() }
.clickable(enabled = !isProcessing) { onClick() }
) {
Row(
modifier = Modifier
@ -167,19 +178,28 @@ fun ServerItem(
)
}
Spacer(modifier = Modifier.height(12.dp))
IconButton(
onClick = onClick,
) {
Box(
if (isProcessing) {
CircularProgressIndicator(
modifier = Modifier
.fillMaxSize()
.size(24.dp),
strokeWidth = 2.dp
)
} else {
IconButton(
onClick = onClick,
) {
Image(
painter = painterResource(R.drawable.icn_arrow_forward_24dp),
contentDescription = "",
Box(
modifier = Modifier
.align(Alignment.Center)
)
.fillMaxSize()
) {
Image(
painter = painterResource(R.drawable.icn_arrow_forward_24dp),
contentDescription = "",
modifier = Modifier
.align(Alignment.Center)
)
}
}
}
}

View File

@ -48,6 +48,14 @@ class DiscoverServersListViewModel @Inject constructor(
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()
// Initialize by loading servers
init {
@ -75,8 +83,8 @@ class DiscoverServersListViewModel @Inject constructor(
}
}
fun loadServerInviteInfo(inviteCode: String): Flow<RsResult<Invite, RevoltError>> = flow {
_isLoading.value = true
fun loadServerInviteInfo(inviteCode: String, serverId: String): Flow<RsResult<Invite, RevoltError>> = flow {
_processingServerId.value = serverId
_error.value = null
try {
@ -86,7 +94,7 @@ class DiscoverServersListViewModel @Inject constructor(
_error.value = e.message ?: "Unknown error occurred"
emit(RsResult.err(RevoltError("Unknown")))
} finally {
_isLoading.value = false
_processingServerId.value = null
}
}.flowOn(Dispatchers.IO)
@ -104,4 +112,38 @@ class DiscoverServersListViewModel @Inject constructor(
_isLoading.value = false
}
}.flowOn(Dispatchers.IO)
/**
* Load server data first, then show the dialog with the loaded data
*/
fun loadServerDataAndShowDialog(inviteCode: String, serverId: String) {
_loadedInviteData.value = null
_processingServerId.value = serverId
_error.value = null
viewModelScope.launch {
try {
val result = fetchInviteByCode(inviteCode)
if (result.ok) {
_loadedInviteData.value = result.value
// Only set the selected invite code after data is loaded
_selectedServerInviteCode.value = inviteCode
} else {
_error.value = result.error?.type ?: "Unknown error occurred"
}
} catch (e: Exception) {
_error.value = e.message ?: "Unknown error occurred"
} finally {
_processingServerId.value = null
}
}
}
/**
* Clear loaded data when dialog is dismissed
*/
fun clearLoadedData() {
_loadedInviteData.value = null
_selectedServerInviteCode.value = null
}
}

View File

@ -1,11 +1,18 @@
package chat.revolt.composables.screens.chat.discover
import android.widget.Space
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
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.padding
import androidx.compose.foundation.layout.height
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
@ -18,14 +25,17 @@ 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.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
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.tooling.preview.Preview
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.RevoltAPI
import chat.revolt.api.RevoltError
import chat.revolt.api.schemas.Invite
import chat.revolt.composables.generic.IconPlaceholder
@ -114,13 +124,25 @@ fun ServerInviteDialog(
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = invite.serverName ?: stringResource(id = R.string.unknown),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
modifier = Modifier.fillMaxWidth()
)
confirmButton = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom // Align buttons to the bottom
) {
Button(
onClick = onJoinClick,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.invite_join))
}
TextButton(
onClick = onDismiss,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.invite_cancel))
}
}
},
text = {
Column(
@ -129,7 +151,7 @@ fun ServerInviteDialog(
) {
if (invite.serverIcon != null) {
RemoteImage(
url = "$REVOLT_FILES/icons/${invite.serverIcon.id}/${invite.serverIcon.filename}",
url = "${RevoltAPI.getCurrentFilesUrl()}/icons/${invite.serverIcon.id}/${invite.serverIcon.filename}",
allowAnimation = false,
description = invite.serverName ?: stringResource(id = R.string.unknown),
modifier = Modifier
@ -152,28 +174,122 @@ fun ServerInviteDialog(
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = invite.serverName ?: stringResource(id = R.string.unknown),
textAlign = TextAlign.Center,
fontSize = 26.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.fillMaxWidth()
)
// Invited by section
if (invite.userName != null) {
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.outlineVariant,
shape = MaterialTheme.shapes.large
)
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = "Invited by")
Row(
verticalAlignment = Alignment.CenterVertically,
) {
if (invite.userAvatar != null) {
RemoteImage(
url = "${RevoltAPI.getCurrentFilesUrl()}/avatars/${invite.userAvatar.id}/${invite.userAvatar.filename}",
description = invite.userName,
modifier = Modifier
.size(24.dp)
.clip(CircleShape)
)
} else {
IconPlaceholder(name = invite.userName, modifier = Modifier.size(24.dp).clip(CircleShape), fontSize = 12.sp)
}
Spacer(modifier = Modifier.width(4.dp))
Text(text = invite.userName, fontWeight = FontWeight.SemiBold)
}
}
}
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
)
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(R.drawable.three_person),
contentDescription = null,
colorFilter = ColorFilter.tint(
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(
alpha = 0.6f
)
)
)
Text(
text = stringResource(
id = R.string.members_count,
invite.memberCount
),
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(
alpha = 0.6f
)
)
}
}
}
},
confirmButton = {
Button(onClick = onJoinClick) {
Text(text = stringResource(id = R.string.invite_join))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(text = stringResource(id = R.string.invite_cancel))
}
}
)
}
}
@Preview
@Composable
fun ServerInviteDialogPreviewLoading() {
ServerInviteDialog(
isLoading = true,
invite = null,
error = null,
onJoinClick = {},
onDismiss = {}
)
}
@Preview
@Composable
fun ServerInviteDialogPreviewError() {
ServerInviteDialog(
isLoading = false,
invite = null,
error = RevoltError(type = "NotFound"),
onJoinClick = {},
onDismiss = {}
)
}
@Preview
@Composable
fun ServerInviteDialogPreviewSuccess() {
val invite = Invite(
serverName = "Revolt Test Server",
serverIcon = null, // Replace with a real AutumnResource if needed for preview
memberCount = 123,
userName = "TestUser",
userAvatar = null // Replace with a real AutumnResource if needed for preview
)
ServerInviteDialog(
isLoading = false,
invite = invite,
error = null,
onJoinClick = {},
onDismiss = {}
)
}

View File

@ -2,6 +2,7 @@ package chat.revolt.composables.screens.chat.discover
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -18,40 +19,32 @@ import kotlinx.coroutines.launch
@Composable
fun ServerInviteHandler(
inviteCode: String,
serverId: String,
viewModel: DiscoverServersListViewModel = viewModel(),
onDismiss: () -> Unit = {},
onJoinSuccess: () -> Unit = {}
) {
val scope = rememberCoroutineScope()
var isLoading by remember { mutableStateOf(true) }
var isJoining by remember { mutableStateOf(false) }
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
}
}
}
// Get the pre-loaded invite data
val loadedInviteData by viewModel.loadedInviteData.collectAsState()
if (showDialog) {
if (showDialog && loadedInviteData != null) {
ServerInviteDialog(
isLoading = isLoading,
invite = inviteResult?.value,
isLoading = isJoining,
invite = loadedInviteData,
error = error,
onJoinClick = {
isLoading = true
isJoining = true
scope.launch {
viewModel.joinServer(inviteCode).collectLatest { result ->
isLoading = false
isJoining = false
if (result.ok) {
showDialog = false
viewModel.clearLoadedData()
onJoinSuccess()
} else {
error = result.error
@ -61,6 +54,7 @@ fun ServerInviteHandler(
},
onDismiss = {
showDialog = false
viewModel.clearLoadedData()
onDismiss()
}
)

View File

@ -543,9 +543,9 @@
<string name="link_type_no_intent">No app found to handle this link</string>
<string name="link_open">Open</string>
<string name="invite_message">You\'ve been invited to join this server. Would you like to join?</string>
<string name="invite_join">Join</string>
<string name="invite_cancel">Cancel</string>
<string name="invite_message">Youve been invited to join</string>
<string name="invite_join">Accept Invite</string>
<string name="invite_cancel">No Thanks</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>