diff --git a/app/src/main/java/chat/revolt/activities/MainActivity.kt b/app/src/main/java/chat/revolt/activities/MainActivity.kt index 448c81e2..60fc404d 100644 --- a/app/src/main/java/chat/revolt/activities/MainActivity.kt +++ b/app/src/main/java/chat/revolt/activities/MainActivity.kt @@ -166,7 +166,7 @@ class MainActivityViewModel @Inject constructor( try { val res = RevoltHttp.get("/".api()) return res.status.value == 200 - } catch (e: Exception) { + } catch (_: Exception) { return false } } @@ -225,7 +225,7 @@ class MainActivityViewModel @Inject constructor( val canReachRevolt = canReachRevolt() val valid = try { RevoltAPI.checkSessionToken(token) - } catch (e: Throwable) { + } catch (_: Throwable) { false } @@ -275,7 +275,7 @@ class MainActivityViewModel @Inject constructor( viewModelScope.launch { kvStorage.remove("sessionToken") kvStorage.remove("sessionId") - startWithDestination("login/greeting") + startWithDestination("choose-platform") } } @@ -430,7 +430,6 @@ class MainActivity : AppCompatActivity() { val RevoltTweenInt: FiniteAnimationSpec = tween(400, easing = EaseInOutExpo) val RevoltTweenFloat: FiniteAnimationSpec = tween(400, easing = EaseInOutExpo) val RevoltTweenDp: FiniteAnimationSpec = tween(400, easing = EaseInOutExpo) -val RevoltTweenColour: FiniteAnimationSpec = tween(400, easing = EaseInOutExpo) val NavTweenInt: FiniteAnimationSpec = tween(350, easing = EaseInOutExpo) val NavTweenFloat: FiniteAnimationSpec = tween(350, easing = EaseInOutExpo) @@ -694,10 +693,11 @@ fun AppEntrypoint( val channelId = backStackEntry.arguments?.getString("channelId") ?: "" ChannelScreen( channelId = channelId, - onToggleDrawer = {}, useDrawer = false, useBackButton = true, - backToChannelsScreen = {}, + backToChannelsScreen = { + navController.navigate("main") + }, backButtonAction = { navController.popBackStack() }, diff --git a/app/src/main/java/chat/revolt/api/RevoltAPI.kt b/app/src/main/java/chat/revolt/api/RevoltAPI.kt index fa1f3718..0e5fb17c 100644 --- a/app/src/main/java/chat/revolt/api/RevoltAPI.kt +++ b/app/src/main/java/chat/revolt/api/RevoltAPI.kt @@ -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 } diff --git a/app/src/main/java/chat/revolt/api/routes/googlesheets/GoogleSheetsService.kt b/app/src/main/java/chat/revolt/api/routes/googlesheets/GoogleSheetsService.kt new file mode 100644 index 00000000..4c2082ac --- /dev/null +++ b/app/src/main/java/chat/revolt/api/routes/googlesheets/GoogleSheetsService.kt @@ -0,0 +1,162 @@ +package chat.revolt.api.routes.googlesheets + +import chat.revolt.api.RevoltHttp +import chat.revolt.api.RevoltJson +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +/** + * Service for fetching and parsing Google Sheets data + * This supports both JSON and CSV formats from published Google Sheets + */ +object GoogleSheetsService { + + /** + * Fetches data from a Google Sheet and maps it to a list of objects of type T + * @param sheetUrl The URL of the published Google Sheet (CSV or JSON) + * @param mapper A function that maps a row (as a Map) to an object of type T + * @return A list of objects of type T + */ + suspend inline fun fetchSheetData( + sheetUrl: String, + crossinline mapper: (Map) -> T + ): List = withContext(Dispatchers.IO) { + try { + val response = RevoltHttp.get(sheetUrl).bodyAsText() + + // Determine if the response is CSV or JSON + if (sheetUrl.contains("output=csv") || response.startsWith("\"") || response.contains(",")) { + parseCSV(response, mapper) + } else { + parseJSON(response, mapper) + } + } catch (e: Exception) { + e.printStackTrace() + emptyList() + } + } + + /** + * Parse CSV data from Google Sheets + */ + inline fun parseCSV( + csvData: String, + crossinline mapper: (Map) -> T + ): List { + // Split the CSV into lines + val lines = csvData.lines().filter { it.isNotBlank() } + if (lines.isEmpty()) return emptyList() + + // Parse header row + val headers = lines[0].split(",").map { + it.trim().removeSurrounding("\"") + } + + // Parse data rows + return lines.drop(1).map { line -> + val values = parseCSVLine(line) + val rowData = mutableMapOf() + + headers.forEachIndexed { index, header -> + if (index < values.size) { + rowData[header] = values[index] + } + } + + mapper(rowData) + } + } + + /** + * Parse a single CSV line, handling quoted values properly + */ + fun parseCSVLine(line: String): List { + val values = mutableListOf() + var currentValue = StringBuilder() + var inQuotes = false + + for (char in line) { + when { + char == '\"' -> inQuotes = !inQuotes + char == ',' && !inQuotes -> { + values.add(currentValue.toString().trim().removeSurrounding("\"")) + currentValue = StringBuilder() + } + else -> currentValue.append(char) + } + } + + // Add the last value + values.add(currentValue.toString().trim().removeSurrounding("\"")) + + return values + } + + /** + * Parse JSON data from Google Sheets + */ + inline fun parseJSON( + jsonData: String, + crossinline mapper: (Map) -> T + ): List { + val sheetResponse = RevoltJson.decodeFromString(jsonData) + + // Convert the sheet data to a list of objects + val headers = sheetResponse.feed.entry + .take(sheetResponse.feed.entry.firstOrNull()?.content?.columnCount ?: 0) + .mapIndexed { index, entry -> + "gsx\$column${index + 1}" to entry.title.text + }.toMap() + + val rows = sheetResponse.feed.entry.chunked(headers.size) + .drop(1) // Skip header row + .map { rowEntries -> + val rowData = mutableMapOf() + rowEntries.forEachIndexed { index, entry -> + val columnName = headers["gsx\$column${index + 1}"] ?: "column${index + 1}" + rowData[columnName] = entry.content.text + } + mapper(rowData) + } + + return rows + } +} + +/** + * Data classes for parsing Google Sheets API response + */ +@Serializable +data class GoogleSheetResponse( + val feed: Feed +) + +@Serializable +data class Feed( + val entry: List +) + +@Serializable +data class Entry( + val title: Title, + val content: Content +) + +@Serializable +data class Title( + @SerialName("\$t") + val text: String +) + +@Serializable +data class Content( + @SerialName("\$t") + val text: String, + @SerialName("columnCount") + val columnCount: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/routes/googlesheets/ServerDataRepository.kt b/app/src/main/java/chat/revolt/api/routes/googlesheets/ServerDataRepository.kt new file mode 100644 index 00000000..7d474c26 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/routes/googlesheets/ServerDataRepository.kt @@ -0,0 +1,51 @@ +package chat.revolt.api.routes.googlesheets + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.serialization.Serializable + +/** + * Repository for fetching server data from a Google Sheet + */ +class ServerDataRepository { + + /** + * Fetches server data from Google Sheets and returns as a Flow + * @param sheetUrl The URL of the published Google Sheet (CSV or JSON) + * @return Flow of ServerData list + */ + fun getServers(sheetUrl: String): Flow> = flow { + val servers = GoogleSheetsService.fetchSheetData( + sheetUrl = sheetUrl + ) { rowData -> + ServerData( + id = rowData["id"] ?: "", + name = rowData["name"] ?: "", + description = rowData["description"] ?: "", + inviteCode = rowData["inviteCode"] ?: "", + disabled = when (rowData["disabled"]?.lowercase()) { + "false" -> false + else -> true + }, + showColor = rowData["showcolor"] + ) + } + emit(servers) + }.flowOn(Dispatchers.IO) + +} + +/** + * Data class representing a server + */ +@Serializable +data class ServerData( + val id: String, + val name: String, + val description: String, + val inviteCode: String, + val disabled: Boolean, + val showColor: String?, +) \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/composables/screens/chat/atoms/RegularMessage.kt b/app/src/main/java/chat/revolt/composables/screens/chat/atoms/RegularMessage.kt index 3ba2a7e4..505102f0 100644 --- a/app/src/main/java/chat/revolt/composables/screens/chat/atoms/RegularMessage.kt +++ b/app/src/main/java/chat/revolt/composables/screens/chat/atoms/RegularMessage.kt @@ -63,7 +63,6 @@ const val SWIPE_TO_REPLY_THRESHOLD = -450f fun RegularMessage( message: Message, channel: Channel?, - drawerIsOpen: Boolean, setDrawerGestureEnabled: (Boolean) -> Unit, setDisableScroll: (Boolean) -> Unit, showMessageBottomSheet: (String) -> Unit, @@ -77,7 +76,6 @@ fun RegularMessage( var offsetX by remember { mutableFloatStateOf(0f) } val animOffsetX by animateFloatAsState( when { - drawerIsOpen -> 0f offsetX > -20f -> 0f else -> offsetX }, @@ -103,7 +101,7 @@ fun RegularMessage( label = "Swipe to Reply indicator foreground" ) - var onFingerMoveHandler: (List) -> Unit = + val onFingerMoveHandler: (List) -> Unit = { changeList: List -> changeList.firstOrNull() ?.let { 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 new file mode 100644 index 00000000..7f0146ed --- /dev/null +++ b/app/src/main/java/chat/revolt/composables/screens/chat/discover/DiscoverServersList.kt @@ -0,0 +1,337 @@ +package chat.revolt.composables.screens.chat.discover + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.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 +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +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.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.graphics.toColorInt +import androidx.hilt.navigation.compose.hiltViewModel +import chat.revolt.R +import chat.revolt.api.RevoltAPI +import chat.revolt.api.routes.googlesheets.ServerData + +@Composable +fun DiscoverServersList( + onJoinToServerSuccess: (String) -> Unit, +) { + val viewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsState() + + // Handle server invite dialog + uiState.selectedInviteCode?.let { inviteCode -> + // Find the server with this invite code + val selectedServer = uiState.servers.find { it.inviteCode == inviteCode } + selectedServer?.let { server -> + ServerInviteHandler( + inviteCode = inviteCode, + viewModel = viewModel, + onDismiss = { + viewModel.setSelectedInviteCode(null) + }, + onJoinSuccess = { serverId -> + onJoinToServerSuccess(serverId) + } + ) + } + } + + Column( + modifier = Modifier + .padding(top = 24.dp, start = 16.dp, end = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = Modifier + .padding(8.dp), + painter = painterResource(R.drawable.discover_character_image), + contentDescription = null, + ) + + Text( + text = stringResource(R.string.discover_servers), + style = MaterialTheme.typography.headlineMedium + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.discover_servers_description), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium + ) + + Spacer(modifier = Modifier.height(16.dp)) + + when { + uiState.isLoading -> { + CircularProgressIndicator( + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterHorizontally) + ) + } + + uiState.error != null -> { + Text(stringResource(R.string.error)) + } + + uiState.servers.isEmpty() -> { + Text(stringResource(R.string.no_servers_found)) + } + + else -> { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .weight(1f) + ) { + items(uiState.servers) { server -> + // Check if the server is already joined + val isJoined = isServerAlreadyJoined(server.id) + + ServerItem( + server = server, + isProcessing = uiState.processingServerId == server.id, + isJoined = isServerAlreadyJoined(server.id), + onClick = { + if (isJoined) { + // If already joined, navigate directly to server channels + onJoinToServerSuccess(server.id) + } else if (server.inviteCode.isNotEmpty()) { + // If not joined, load server data and show invite dialog + viewModel.loadServerDataAndShowDialog( + server.inviteCode, + server.id + ) + } + } + ) + } + } + } + } + } +} + +private fun isServerAlreadyJoined(serverId: String): Boolean { + // Check if the server exists in RevoltAPI.serverCache + return RevoltAPI.serverCache.containsKey(serverId) +} + +@Composable +fun ServerItem( + server: ServerData, + isJoined: Boolean, + onClick: () -> Unit, + isProcessing: Boolean = false +) { + // Safely convert the hex string to a Color, or return null if invalid + fun parseColorOrNull(hex: String?): Color? { + return hex?.let { raw -> + val normalized = if (raw.startsWith("#")) raw else "#$raw" + try { + Color(normalized.toColorInt()) + } catch (_: IllegalArgumentException) { + null + } + } + } + + // Determine card colors based on server.showColor + val containerColor = parseColorOrNull(server.showColor) + val cardColors = if (containerColor != null) { + CardDefaults.cardColors(containerColor = containerColor) + } else { + CardDefaults.cardColors() + } + + // Calculate alpha values based on disabled state + val contentAlpha = if (server.disabled) 0.5f else 1.0f + val descriptionAlpha = if (server.disabled) 0.4f else 0.7f + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .clickable(enabled = !isProcessing) { onClick() }, + colors = cardColors, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.three_person), + contentDescription = "", + colorFilter = ColorFilter.tint( + color = MaterialTheme.colorScheme.onBackground.copy(alpha = contentAlpha) + ) + ) + Spacer(modifier = Modifier.width(12.dp)) + + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = server.name, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onBackground.copy( + alpha = contentAlpha + ) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = server.description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground.copy( + alpha = descriptionAlpha, + ), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.height(12.dp)) + + if (isProcessing) { + CircularProgressIndicator( + modifier = Modifier + .size(24.dp), + strokeWidth = 2.dp + ) + } else { + IconButton( + onClick = onClick, + ) { + Box( + modifier = Modifier + .fillMaxSize() + ) { + if (isJoined) { + // Show tick icon with green background for joined servers + Box( + modifier = Modifier + .size(20.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary) // Green color + .align(Alignment.Center), + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.icn_check_24dp), + contentDescription = "Already joined", + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimary) + ) + } + } else { + // Show arrow icon for servers not joined yet + Image( + painter = painterResource(R.drawable.icn_arrow_forward_24dp), + contentDescription = "Join server", + modifier = Modifier + .align(Alignment.Center), + colorFilter = ColorFilter.tint( + color = MaterialTheme.colorScheme.onBackground.copy(alpha = contentAlpha) + ) + ) + } + } + } + } + } + } +} + +@Preview +@Composable +private fun ServerItemPreview() { + val server = ServerData( + id = "1", + name = "Revolt Lounge", + description = "The official Revolt server. Hang out, chat, and get help with Revolt.", + inviteCode = "revolt", + disabled = false, + showColor = null + ) + ServerItem( + server = server, + onClick = {}, + isJoined = false, + isProcessing = false + ) +} + +@Preview +@Composable +private fun ColorfulServerItemPreview() { + val server = ServerData( + id = "1", + name = "Revolt Lounge", + description = "The official Revolt server. Hang out, chat, and get help with Revolt.", + inviteCode = "revolt", + disabled = false, + showColor = "#1591ea" + ) + ServerItem( + server = server, + onClick = {}, + isJoined = false, + isProcessing = false + ) +} + + +@Preview +@Composable +private fun JoinedServerItemPreview() { + val server = ServerData( + id = "1", + name = "Revolt Lounge", + description = "The official Revolt server. Hang out, chat, and get help with Revolt.", + inviteCode = "revolt", + disabled = false, + showColor = null, + ) + ServerItem( + server = server, + onClick = {}, + isJoined = true, + isProcessing = false + ) +} \ No newline at end of file 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 new file mode 100644 index 00000000..ae55a2ed --- /dev/null +++ b/app/src/main/java/chat/revolt/composables/screens/chat/discover/DiscoverServersListViewModel.kt @@ -0,0 +1,157 @@ +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 + +/** + * 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() { + + // Single UI state + private val _uiState = MutableStateFlow(DiscoverUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + // Initialize + init { + loadServers() + } + + /** + * Load server list from repository + */ + fun loadServers() { + updateState { it.copy(isLoading = true, error = null) } + + viewModelScope.launch { + try { + 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 -> + updateState { it.copy(servers = servers, isLoading = false) } + } + } catch (e: Exception) { + updateState { + it.copy( + error = e.message ?: "Unknown error occurred", + isLoading = false + ) + } + } + } + } + + /** + * Load server data and show dialog when ready + */ + fun loadServerDataAndShowDialog(inviteCode: String, serverId: String) { + updateState { + it.copy( + loadedInviteData = null, + processingServerId = serverId + ) + } + + viewModelScope.launch { + try { + val result = fetchInviteByCode(inviteCode) + if (result.ok) { + updateState { + it.copy( + loadedInviteData = result.value, + selectedInviteCode = inviteCode, + processingServerId = null + ) + } + } else { + updateState { + it.copy( + loadedError = result.error, + selectedInviteCode = inviteCode, + processingServerId = null + ) + } + } + } catch (_: Exception) { + updateState { + it.copy( + loadedError = RevoltError("Unknown"), + selectedInviteCode = inviteCode, + processingServerId = 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() { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/composables/screens/chat/discover/ServerInviteDialog.kt b/app/src/main/java/chat/revolt/composables/screens/chat/discover/ServerInviteDialog.kt new file mode 100644 index 00000000..88472408 --- /dev/null +++ b/app/src/main/java/chat/revolt/composables/screens/chat/discover/ServerInviteDialog.kt @@ -0,0 +1,273 @@ +package chat.revolt.composables.screens.chat.discover + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +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.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.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.RevoltAPI +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 (error != null) { + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Image( + painter = painterResource(R.drawable.link_no_valid_img), + contentDescription = null, + ) + }, + title = { + Text( + text = stringResource(id = R.string.invite_error_header), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + }, + text = { + Column { + 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() + ) + Spacer(Modifier.height(32.dp)) + Button( + onClick = onDismiss, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = stringResource(id = R.string.spark_early_access_cta)) + } + } + }, + confirmButton = {} + ) + return + } + + if (invite == null) { + // This shouldn't happen, but handle it just in case + return + } + + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Bottom // Align buttons to the bottom + ) { + Button( + onClick = onJoinClick, + modifier = Modifier.fillMaxWidth(), + enabled = !isLoading + ) { + if (isLoading) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = stringResource(id = R.string.joining)) + } + } else { + Text(text = stringResource(id = R.string.invite_join)) + } + } + TextButton( + onClick = onDismiss, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.invite_cancel)) + } + } + }, + text = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + if (invite.serverIcon != null) { + RemoteImage( + url = "${RevoltAPI.getCurrentFilesUrl()}/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 + ) + + 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)) + 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 + ) + ) + } + } + } + }, + ) +} + + +@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 = {} + ) +} + 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 new file mode 100644 index 00000000..c63b555c --- /dev/null +++ b/app/src/main/java/chat/revolt/composables/screens/chat/discover/ServerInviteHandler.kt @@ -0,0 +1,61 @@ +package chat.revolt.composables.screens.chat.discover + +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.hilt.navigation.compose.hiltViewModel +import chat.revolt.api.RevoltError +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@Composable +fun ServerInviteHandler( + inviteCode: String, + viewModel: DiscoverServersListViewModel = hiltViewModel(), + onDismiss: () -> Unit = {}, + onJoinSuccess: (String) -> Unit = {} +) { + val scope = rememberCoroutineScope() + var isJoining by remember { mutableStateOf(false) } + var showDialog by remember { mutableStateOf(true) } + var error by remember { mutableStateOf(null) } + + // Get the UI state + val uiState by viewModel.uiState.collectAsState() + + if (showDialog) { + ServerInviteDialog( + isLoading = isJoining, + invite = uiState.loadedInviteData, + error = error ?: uiState.loadedError, + onJoinClick = { + isJoining = true + scope.launch { + // Don't set the processing server ID here to avoid showing loading in the list + viewModel.joinServerWithoutProcessingIndicator(inviteCode).collectLatest { result -> + isJoining = false + if (result.ok) { + showDialog = false + viewModel.clearLoadedData() + uiState.loadedInviteData?.serverId?.let { serverId -> + onJoinSuccess(serverId) + } + + } else { + error = result.error + } + } + } + }, + onDismiss = { + showDialog = false + viewModel.clearLoadedData() + onDismiss() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/composables/screens/chat/drawer/ChannelSideDrawer.kt b/app/src/main/java/chat/revolt/composables/screens/chat/drawer/ChannelSideDrawer.kt index fb6a1770..65bd40fc 100644 --- a/app/src/main/java/chat/revolt/composables/screens/chat/drawer/ChannelSideDrawer.kt +++ b/app/src/main/java/chat/revolt/composables/screens/chat/drawer/ChannelSideDrawer.kt @@ -1,9 +1,7 @@ package chat.revolt.composables.screens.chat.drawer -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.VisibilityThreshold import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring @@ -30,13 +28,13 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -53,7 +51,6 @@ import androidx.compose.runtime.derivedStateOf 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.Alignment import androidx.compose.ui.Modifier @@ -69,10 +66,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.navigation.NavController import chat.revolt.R import chat.revolt.api.RevoltAPI import chat.revolt.api.internals.CategorisedChannelList @@ -94,8 +89,8 @@ import chat.revolt.composables.generic.RemoteImage import chat.revolt.composables.generic.UserAvatar import chat.revolt.composables.generic.presenceFromStatus import chat.revolt.composables.screens.chat.ChannelIcon +import chat.revolt.composables.screens.chat.discover.DiscoverServersList import chat.revolt.screens.chat.ChatRouterDestination -import chat.revolt.screens.chat.LocalIsConnected import chat.revolt.sheets.ChannelContextSheet @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @@ -109,10 +104,10 @@ fun ChannelSideDrawer( onShowServerContextSheet: (String) -> Unit, showSettingsIcon: Boolean, onOpenSettings: () -> Unit, - topNav: NavController, onShowAddServerSheet: () -> Unit, modifier: Modifier = Modifier ) { + val server = RevoltAPI.serverCache[currentServer] val categorisedChannels = server?.let { ChannelUtils.categoriseServerFlat(it) @@ -122,7 +117,6 @@ fun ChannelSideDrawer( LaunchedEffect(currentDestination) { if ((currentDestination is ChatRouterDestination.ServersChannels) && currentServer != null) { val channelIndex = categorisedChannels?.indexOfFirst { - when (it) { is CategorisedChannelList.Channel -> it.channel.id == currentDestination.serverID else -> false @@ -153,17 +147,6 @@ fun ChannelSideDrawer( ), label = "Server banner height" ) - val serverInfoOffset by animateDpAsState( - if (LocalIsConnected.current) - WindowInsets.statusBars.asPaddingValues().calculateTopPadding() - else - 0.dp, - animationSpec = spring( - stiffness = Spring.StiffnessMediumLow, - visibilityThreshold = Dp.VisibilityThreshold - ) - ) - // - Take the list of servers and filter them by the ones that are in the ordering. // - Sort the servers that are in the ordering using the ordering. // - Add the servers that aren't in the ordering to the end of the list. @@ -200,28 +183,14 @@ fun ChannelSideDrawer( } } - val scope = rememberCoroutineScope() - Row(modifier.fillMaxSize()) { LazyColumn( Modifier.width(64.dp), verticalArrangement = Arrangement.spacedBy(6.dp), horizontalAlignment = Alignment.CenterHorizontally, - contentPadding = PaddingValues( - bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - ) ) { stickyHeader(key = "self") { Column(Modifier.background(MaterialTheme.colorScheme.background)) { - AnimatedVisibility(LocalIsConnected.current) { - Spacer( - Modifier - .height( - WindowInsets.statusBars.asPaddingValues() - .calculateTopPadding() - ) - ) - } UserAvatar( username = RevoltAPI.userCache[RevoltAPI.selfId]?.let { User.resolveDefaultName( @@ -238,7 +207,7 @@ fun ChannelSideDrawer( size = 48.dp, presenceSize = 16.dp, onClick = { - onDestinationChanged(ChatRouterDestination.defaultForDMList) + onDestinationChanged(ChatRouterDestination.Home) }, onLongClick = onLongPressAvatar, modifier = Modifier @@ -315,6 +284,47 @@ fun ChannelSideDrawer( ) } + + item(key = "discover") { + Box( + Modifier + .padding(8.dp) + .clip(RoundedCornerShape(16.dp)) + .clickable { + onDestinationChanged( + ChatRouterDestination.Discover + ) + } + .size(48.dp) + .background(MaterialTheme.colorScheme.onPrimary), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(R.drawable.icn_explore_24dp), + contentDescription = stringResource(R.string.discover_alt) + ) + } + } + + item(key = "add_server") { + Box( + Modifier + .padding(8.dp) + .clip(CircleShape) + .clickable { + onShowAddServerSheet() + } + .size(48.dp) + .background(MaterialTheme.colorScheme.onPrimary), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(R.drawable.icn_add_24dp), + contentDescription = stringResource(R.string.server_plus_alt) + ) + } + } + items( serverList.size, key = { serverList[it].id ?: it } @@ -389,42 +399,6 @@ fun ChannelSideDrawer( } } - item(key = "add_server") { - Box( - Modifier - .padding(8.dp) - .clip(CircleShape) - .clickable { - onShowAddServerSheet() - } - .size(48.dp), - contentAlignment = Alignment.Center - ) { - Icon( - painter = painterResource(R.drawable.icn_add_24dp), - contentDescription = stringResource(R.string.server_plus_alt) - ) - } - } - - item(key = "discover") { - Box( - Modifier - .padding(8.dp) - .clip(CircleShape) - .clickable { - topNav.navigate("discover") - } - .size(48.dp), - contentAlignment = Alignment.Center - ) { - Icon( - painter = painterResource(R.drawable.icn_explore_24dp), - contentDescription = stringResource(R.string.discover_alt) - ) - } - } - if (showSettingsIcon) { item(key = "settings") { Box( @@ -447,23 +421,14 @@ fun ChannelSideDrawer( } Column( Modifier - .background(MaterialTheme.colorScheme.surfaceContainer) - .weight(1f) + .clip(shape = RoundedCornerShape(topStart = 24.dp)) + .background(color = MaterialTheme.colorScheme.surfaceContainer) + .weight(weight = 1f) .fillMaxHeight() ) { Box( Modifier - .clip( - MaterialTheme.shapes.medium.copy( - topStart = CornerSize(0.dp), - topEnd = CornerSize(0.dp) - ) - ) - .height( - serverBannerHeight + WindowInsets.statusBars.asPaddingValues() - .calculateTopPadding() - ) - //.offset(y = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) + .height(serverBannerHeight) ) { if (server?.banner != null) { RemoteImage( @@ -495,8 +460,7 @@ fun ChannelSideDrawer( Row( Modifier - .padding(16.dp) - .offset(y = serverInfoOffset), + .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { CompositionLocalProvider( @@ -538,7 +502,7 @@ fun ChannelSideDrawer( Text( text = when (currentServer) { - null -> stringResource(R.string.direct_messages) + null -> stringResource(if (currentDestination is ChatRouterDestination.Discover) R.string.discover_servers else R.string.direct_messages) else -> server?.name ?: stringResource(R.string.unknown) }, style = MaterialTheme.typography.titleMedium, @@ -563,30 +527,35 @@ fun ChannelSideDrawer( } } } - - if (currentServer == null) { - DirectMessagesChannelListRenderer( - currentDestination, - onDestinationChanged, - channelListState, - onOpenChannelContextSheet = { channelContextSheetTarget = it } + if (currentDestination is ChatRouterDestination.Discover) { + DiscoverServersList( + onJoinToServerSuccess = navigateToServer ) } else { - ServerChannelListRenderer( - categorisedChannels, - currentDestination, - onDestinationChanged, - channelListState, - onOpenChannelContextSheet = { channelContextSheetTarget = it }, - serverId = currentServer - ) + if (currentServer == null) { + DirectMessagesChannelListRenderer( + currentDestination, + onDestinationChanged, + channelListState, + onOpenChannelContextSheet = { channelContextSheetTarget = it } + ) + } else { + ServerChannelListRenderer( + categorisedChannels, + currentDestination, + onDestinationChanged, + channelListState, + onOpenChannelContextSheet = { channelContextSheetTarget = it }, + serverId = currentServer + ) + } } } } } @Composable -fun ColumnScope.DirectMessagesChannelListRenderer( +private fun ColumnScope.DirectMessagesChannelListRenderer( currentDestination: ChatRouterDestination, onDestinationChanged: (ChatRouterDestination) -> Unit, channelListState: LazyListState, @@ -726,8 +695,9 @@ fun ColumnScope.DirectMessagesChannelListRenderer( } } + @Composable -fun ColumnScope.ServerChannelListRenderer( +private fun ColumnScope.ServerChannelListRenderer( categorisedChannels: List?, currentDestination: ChatRouterDestination, onDestinationChanged: (ChatRouterDestination) -> Unit, diff --git a/app/src/main/java/chat/revolt/di/GoogleSheetsModule.kt b/app/src/main/java/chat/revolt/di/GoogleSheetsModule.kt new file mode 100644 index 00000000..2825e080 --- /dev/null +++ b/app/src/main/java/chat/revolt/di/GoogleSheetsModule.kt @@ -0,0 +1,26 @@ +package chat.revolt.di + +import chat.revolt.api.routes.googlesheets.ServerDataRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * Dagger Hilt module for providing Google Sheets related dependencies + */ +@Module +@InstallIn(SingletonComponent::class) +object GoogleSheetsModule { + + /** + * Provides a singleton instance of ServerDataRepository + * @return ServerDataRepository instance + */ + @Provides + @Singleton + fun provideServerDataRepository(): ServerDataRepository { + return ServerDataRepository() + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt index 2fab97f9..e01f0eb2 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.icons.Icons @@ -24,7 +25,6 @@ import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Person import androidx.compose.material3.AlertDialog import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -108,24 +108,13 @@ sealed class ChatRouterDestination { data object Settings : ChatRouterDestination() data object Friends : ChatRouterDestination() data object Home : ChatRouterDestination() + data object Discover : ChatRouterDestination() data class Channel(val channelId: String) : ChatRouterDestination() data class ServersChannels(val serverID: String) : ChatRouterDestination() data class NoCurrentChannel(val serverId: String?) : ChatRouterDestination() - fun asSerialisedString(): String { - return when (this) { - is Settings -> "overview" - is Friends -> "friends" - is Channel -> "channel/$channelId" - is ServersChannels -> "channel/$serverID/servers" - is NoCurrentChannel -> "no_current_channel/$serverId" - ChatRouterDestination.Home -> "home" - } - } - companion object { val default = Settings - val defaultForDMList = Settings fun fromString(destination: String): ChatRouterDestination { return when { @@ -198,17 +187,6 @@ class ChatRouterViewModel @Inject constructor( fun setSaveDestination(destination: ChatRouterDestination) { currentDestination = destination - - viewModelScope.launch { - kvStorage.set("currentDestination", destination.asSerialisedString()) - - if (destination is ChatRouterDestination.Channel) { - val server = RevoltAPI.channelCache[destination.channelId]?.server - if (server != null) { - kvStorage.set("lastChannel/$server", destination.channelId) - } - } - } } fun setRegisterForNotifications() { @@ -253,13 +231,7 @@ class ChatRouterViewModel @Inject constructor( fun navigateToServer(serverId: String) { viewModelScope.launch { - val savedLastChannel = kvStorage.get("lastChannel/$serverId") - val channelId = - savedLastChannel ?: RevoltAPI.serverCache[serverId]?.channels?.firstOrNull() - val channelExists = RevoltAPI.channelCache.containsKey(channelId) - - setSaveDestination(ChatRouterDestination.ServersChannels(serverId)) -// + setSaveDestination(ChatRouterDestination.ServersChannels(serverId)) } } } @@ -409,7 +381,11 @@ fun ChatRouterScreen( return@let } - viewModel.setSaveDestination(ChatRouterDestination.Channel(action.channelId)) + viewModel.setSaveDestination( + ChatRouterDestination.Channel( + action.channelId + ) + ) } is Action.LinkInfo -> { @@ -818,14 +794,14 @@ fun ChatRouterScreen( toggleDrawerLambda() }, onShowStatusSheet = { - showStatusSheet = true + showStatusSheet = true }, onShowServerContextSheet = { - serverContextSheetTarget = it - showServerContextSheet = true + serverContextSheetTarget = it + showServerContextSheet = true }, onShowAddServerSheet = { - showAddServerSheet = true + showAddServerSheet = true }, isTouchExplorationEnabled = isTouchExplorationEnabled, viewModel = viewModel, @@ -835,32 +811,6 @@ fun ChatRouterScreen( } } -@Composable -fun Sidebar( - viewModel: ChatRouterViewModel, - currentServer: String?, - topNav: NavController, - onShowStatusSheet: () -> Unit, - navigateToServer: (String) -> Unit, - onShowServerContextSheet: (String) -> Unit, - onShowAddServerSheet: () -> Unit, - showSettingsButton: Boolean, - onOpenSettings: () -> Unit, -) { - ChannelSideDrawer( - onDestinationChanged = viewModel::setSaveDestination, - currentDestination = viewModel.currentDestination, - currentServer = currentServer, - navigateToServer = navigateToServer, - onLongPressAvatar = onShowStatusSheet, - onShowServerContextSheet = onShowServerContextSheet, - showSettingsIcon = showSettingsButton, - onOpenSettings = onOpenSettings, - topNav = topNav, - onShowAddServerSheet = onShowAddServerSheet - ) -} - @Composable fun ChannelNavigator( dest: ChatRouterDestination, @@ -872,138 +822,132 @@ fun ChannelNavigator( currentServer: String?, toggleDrawer: () -> Unit, isTouchExplorationEnabled: Boolean, - drawerState: DrawerState? = null, setDrawerGestureEnabled: (Boolean) -> Unit = {}, ) { - val scope = rememberCoroutineScope() - - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - BottomAppBar { - NavigationBarItem( - icon = { - Icon( - imageVector = Icons.Default.Home, - contentDescription = "Home", + when (dest) { + is ChatRouterDestination.Channel -> { + ChannelScreen( + channelId = dest.channelId, + backToChannelsScreen = { + currentServer?.let { + viewModel.setSaveDestination( + ChatRouterDestination.ServersChannels( + serverID = currentServer + ) ) - }, - label = { - Text(text = "you") - }, - selected = dest is ChatRouterDestination.Home, - enabled = true, - onClick = { - viewModel.setSaveDestination(ChatRouterDestination.Home) - } - ) - NavigationBarItem( - icon = { - Icon( - imageVector = Icons.Default.Person, - contentDescription = "Friends", - ) - }, - label = { - Text(text = "Friends") - }, - selected = dest is ChatRouterDestination.Friends, - enabled = true, - onClick = { - viewModel.setSaveDestination(ChatRouterDestination.Friends) - } - ) - NavigationBarItem( - icon = { - Icon( - imageVector = Icons.Default.Face, - contentDescription = "You", - ) - }, - label = { - Text(text = "you") - }, - selected = dest is ChatRouterDestination.Settings, - enabled = true, - onClick = { - viewModel.setSaveDestination(ChatRouterDestination.Settings) - } - ) - } + } ?: viewModel.setSaveDestination( + ChatRouterDestination.Home + ) + }, + setDrawerGestureEnabled = setDrawerGestureEnabled, + ) } - ) { innerPadding -> - Column { - when (dest) { - is ChatRouterDestination.Settings -> { - SettingsScreen( - navController = topNav, - ) - } - is ChatRouterDestination.Friends -> { - FriendsScreen( - topNav = topNav, - onDrawerClicked = toggleDrawer, - ) - } - - is ChatRouterDestination.Home -> { - Sidebar( - viewModel = viewModel, - topNav = topNav, - currentServer = currentServer, - onShowStatusSheet = onShowStatusSheet, - navigateToServer = viewModel::navigateToServer, - onShowServerContextSheet = onShowServerContextSheet, - onShowAddServerSheet = onShowAddServerSheet, - showSettingsButton = isTouchExplorationEnabled, - onOpenSettings = { - topNav.navigate("settings") - }, - ) - } - is ChatRouterDestination.ServersChannels-> { - Sidebar( - viewModel = viewModel, - topNav = topNav, - currentServer = dest.serverID, - onShowStatusSheet = onShowStatusSheet, - navigateToServer = viewModel::navigateToServer, - onShowServerContextSheet = onShowServerContextSheet, - onShowAddServerSheet = onShowAddServerSheet, - showSettingsButton = isTouchExplorationEnabled, - onOpenSettings = { - topNav.navigate("settings") - }, - ) - } - - is ChatRouterDestination.Channel -> { - ChannelScreen( - channelId = dest.channelId, - backToChannelsScreen = { - currentServer?.let { - viewModel.setSaveDestination(ChatRouterDestination.ServersChannels( - serverID = currentServer - )) + else -> { + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + BottomAppBar { + NavigationBarItem( + icon = { + Icon( + imageVector = Icons.Default.Home, + contentDescription = "Home", + ) + }, + label = { + Text(text = "you") + }, + selected = when(dest){ + is ChatRouterDestination.ServersChannels, + is ChatRouterDestination.NoCurrentChannel, + ChatRouterDestination.Home, + ChatRouterDestination.Discover -> true + else -> false + } , + enabled = true, + onClick = { + viewModel.setSaveDestination(ChatRouterDestination.Home) } - }, - onToggleDrawer = { - scope.launch { - if (drawerState?.isOpen == true) { - drawerState.close() - } else { - drawerState?.open() - } + ) + NavigationBarItem( + icon = { + Icon( + imageVector = Icons.Default.Person, + contentDescription = "Friends", + ) + }, + label = { + Text(text = "Friends") + }, + selected = dest is ChatRouterDestination.Friends, + enabled = true, + onClick = { + viewModel.setSaveDestination(ChatRouterDestination.Friends) } - }, - setDrawerGestureEnabled = setDrawerGestureEnabled, - drawerIsOpen = drawerState?.isOpen == true, - ) + ) + NavigationBarItem( + icon = { + Icon( + imageVector = Icons.Default.Face, + contentDescription = "You", + ) + }, + label = { + Text(text = "you") + }, + selected = dest is ChatRouterDestination.Settings, + enabled = true, + onClick = { + viewModel.setSaveDestination(ChatRouterDestination.Settings) + } + ) + } } + ) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding)) { + when (dest) { + is ChatRouterDestination.Settings -> { + SettingsScreen( + navController = topNav, + ) + } - is ChatRouterDestination.NoCurrentChannel -> { - NoCurrentChannelScreen(onDrawerClicked = toggleDrawer) + is ChatRouterDestination.Friends -> { + FriendsScreen( + topNav = topNav, + onDrawerClicked = toggleDrawer, + ) + } + + is ChatRouterDestination.Home, + is ChatRouterDestination.ServersChannels, + is ChatRouterDestination.Discover -> { + ChannelSideDrawer( + onDestinationChanged = viewModel::setSaveDestination, + currentDestination = viewModel.currentDestination, + currentServer = when (dest) { + is ChatRouterDestination.Home -> currentServer + is ChatRouterDestination.ServersChannels -> dest.serverID + else -> null + }, + navigateToServer = viewModel::navigateToServer, + onLongPressAvatar = onShowStatusSheet, + onShowServerContextSheet = onShowServerContextSheet, + showSettingsIcon = isTouchExplorationEnabled, + onOpenSettings = { + topNav.navigate("settings") + }, + onShowAddServerSheet = onShowAddServerSheet + ) + } + + is ChatRouterDestination.Channel -> {} + + is ChatRouterDestination.NoCurrentChannel -> { + NoCurrentChannelScreen(onDrawerClicked = toggleDrawer) + } + } } } } diff --git a/app/src/main/java/chat/revolt/screens/chat/views/FriendsScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/FriendsScreen.kt index 5906c657..6b242e69 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/FriendsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/FriendsScreen.kt @@ -4,7 +4,6 @@ import android.content.Context import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable @@ -12,14 +11,10 @@ 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.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState @@ -100,7 +95,6 @@ import chat.revolt.composables.generic.CountableListHeader import chat.revolt.composables.generic.UserAvatar import chat.revolt.internals.extensions.zero import chat.revolt.markdown.jbm.asHexString -import chat.revolt.screens.chat.LocalIsConnected import io.github.g00fy2.quickie.QRResult import io.github.g00fy2.quickie.ScanQRCode import kotlinx.coroutines.Dispatchers @@ -474,72 +468,61 @@ fun FriendsScreen(topNav: NavController, useDrawer: Boolean = false, onDrawerCli Scaffold( topBar = { - Column { - AnimatedVisibility(LocalIsConnected.current) { - Spacer( - Modifier - .height( - WindowInsets.statusBars.asPaddingValues() - .calculateTopPadding() - ) + TopAppBar( + title = { + Text( + text = stringResource(R.string.friends), + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) - } - TopAppBar( - title = { - Text( - text = stringResource(R.string.friends), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - navigationIcon = { - if (useDrawer) { - IconButton(onClick = { - onDrawerClicked() - }) { - Icon( - painter = painterResource(R.drawable.icn_menu_24dp), - contentDescription = stringResource(id = R.string.menu) - ) - } - } - }, - actions = { + }, + navigationIcon = { + if (useDrawer) { IconButton(onClick = { - overflowMenuShown = true + onDrawerClicked() }) { Icon( - painter = painterResource(R.drawable.icn_more_vert_24dp), - contentDescription = stringResource(R.string.menu) + painter = painterResource(R.drawable.icn_menu_24dp), + contentDescription = stringResource(id = R.string.menu) ) } - DropdownMenu( - expanded = overflowMenuShown, - onDismissRequest = { - overflowMenuShown = false - } - ) { - DropdownMenuItem( - text = { - Text(stringResource(R.string.friends_deny_all_incoming)) - }, - onClick = { + } + }, + actions = { + IconButton(onClick = { + overflowMenuShown = true + }) { + Icon( + painter = painterResource(R.drawable.icn_more_vert_24dp), + contentDescription = stringResource(R.string.menu) + ) + } + DropdownMenu( + expanded = overflowMenuShown, + onDismissRequest = { + overflowMenuShown = false + } + ) { + DropdownMenuItem( + text = { + Text(stringResource(R.string.friends_deny_all_incoming)) + }, + onClick = { + scope.launch { + overflowMenuShown = false + } + with(Dispatchers.IO) { scope.launch { - overflowMenuShown = false - } - with(Dispatchers.IO) { - scope.launch { - FriendRequests.getIncoming() - .forEach { it.id?.let { id -> unfriendUser(id) } } - } + FriendRequests.getIncoming() + .forEach { it.id?.let { id -> unfriendUser(id) } } } } - ) - } - }, - windowInsets = WindowInsets.zero - ) - } + } + ) + } + }, + windowInsets = WindowInsets.zero + ) }, ) { pv -> Box( diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt index 728d4d58..fa55778e 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt @@ -189,12 +189,10 @@ private const val NOT_ENOUGH_SPACE_FOR_PANES_THRESHOLD = 500 @Composable fun ChannelScreen( channelId: String, - onToggleDrawer: () -> Unit, useDrawer: Boolean = false, backToChannelsScreen: (() -> Unit)?, useBackButton: Boolean = false, setDrawerGestureEnabled: (Boolean) -> Unit = {}, - drawerIsOpen: Boolean = false, backButtonAction: (() -> Unit)? = null, useChatUI: Boolean = false, viewModel: ChannelScreenViewModel = hiltViewModel() @@ -629,7 +627,9 @@ fun ChannelScreen( windowInsets = if (useChatUI) WindowInsets.statusBars else WindowInsets.zero, navigationIcon = { if (useDrawer) { - IconButton(onClick = onToggleDrawer) { + IconButton(onClick = { + backToChannelsScreen?.invoke() + }) { Icon( painter = painterResource(R.drawable.icn_menu_24dp), contentDescription = stringResource(id = R.string.menu) @@ -650,7 +650,7 @@ fun ChannelScreen( } ) { pv -> if (viewModel.showGeoGate) { - ChannelScreenGeoGate { onToggleDrawer() } + ChannelScreenGeoGate { backToChannelsScreen?.invoke() } } else { Crossfade( targetState = viewModel.ageGateUnlocked, @@ -665,10 +665,11 @@ fun ChannelScreen( } }, onDeny = { - onToggleDrawer() + backToChannelsScreen?.invoke() } ) } + null -> { Box( contentAlignment = Alignment.Center, @@ -677,6 +678,7 @@ fun ChannelScreen( CircularProgressIndicator(modifier = Modifier.size(48.dp)) } } + true -> { Column( modifier = Modifier @@ -731,7 +733,6 @@ fun ChannelScreen( RegularMessage( item.message, viewModel.channel, - drawerIsOpen = drawerIsOpen, setDrawerGestureEnabled = { setDrawerGestureEnabled(it) }, @@ -972,7 +973,11 @@ fun ChannelScreen( ReplyManager( replies = viewModel.draftReplyTo, onToggleMention = { - scope.launch { viewModel.toggleMentionOnReply(it.id) } + scope.launch { + viewModel.toggleMentionOnReply( + it.id + ) + } }, onRemove = { viewModel.draftReplyTo.remove(it) @@ -1027,7 +1032,9 @@ fun ChannelScreen( trailingIcon = { Icon( painter = painterResource(R.drawable.icn_close_24dp), - contentDescription = stringResource(R.string.message_field_editing_message_cancel_alt), + contentDescription = stringResource( + R.string.message_field_editing_message_cancel_alt + ), tint = MaterialTheme.colorScheme.onSurface, modifier = Modifier.alpha(0.8f) ) @@ -1090,7 +1097,8 @@ fun ChannelScreen( DropdownMenu( expanded = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker && notEnoughSpaceForPanes, onDismissRequest = { - viewModel.activePane = ChannelScreenActivePane.None + viewModel.activePane = + ChannelScreenActivePane.None } ) { DropdownMenuItem( diff --git a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt index 5e8e51c9..8b7eac44 100644 --- a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt @@ -10,12 +10,11 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.ListItem import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -65,22 +64,20 @@ fun SettingsScreen( val context = LocalContext.current val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - Scaffold( + Column( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - LargeTopAppBar( - scrollBehavior = scrollBehavior, - title = { - Text( - text = stringResource(R.string.settings), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - ) - }, - ) { pv -> - Box(Modifier.padding(pv)) { + ) { + TopAppBar( + scrollBehavior = scrollBehavior, + title = { + Text( + text = stringResource(R.string.settings), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + ) + Box { Column( modifier = Modifier .fillMaxSize() @@ -377,7 +374,6 @@ fun SettingsScreen( } }, modifier = Modifier - .padding(bottom =92.dp) .testTag("settings_view_logout") .clickable { viewModel.logout() diff --git a/app/src/main/res/drawable/discover_character_image.xml b/app/src/main/res/drawable/discover_character_image.xml new file mode 100644 index 00000000..e5a4310c --- /dev/null +++ b/app/src/main/res/drawable/discover_character_image.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/icn_check_24dp.xml b/app/src/main/res/drawable/icn_check_24dp.xml index 280f0bd8..c9f56cbf 100644 --- a/app/src/main/res/drawable/icn_check_24dp.xml +++ b/app/src/main/res/drawable/icn_check_24dp.xml @@ -1,10 +1,10 @@ + - - + android:viewportWidth="24" + android:viewportHeight="24"> + + \ No newline at end of file diff --git a/app/src/main/res/drawable/link_no_valid_img.xml b/app/src/main/res/drawable/link_no_valid_img.xml new file mode 100644 index 00000000..ac1fffc5 --- /dev/null +++ b/app/src/main/res/drawable/link_no_valid_img.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/three_person.xml b/app/src/main/res/drawable/three_person.xml new file mode 100644 index 00000000..01641e09 --- /dev/null +++ b/app/src/main/res/drawable/three_person.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index ee109e9f..df923e30 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -157,6 +157,7 @@ يجب أن يكون عمرك 18 عاما أو أكثر لعرض هذه القناة. لا يمكنك أن تولد في المستقبل. الرسائل الخاصة + اكتشف الخوادم رسالة خاصة قناة نصية قناة صوتية diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index ae726ab2..d48a3a5a 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -171,6 +171,7 @@ Vyberte si server zleva a začněte. Pokud se cítíte odvážně, můžete si vytvořit nový server. Avatar uživatele %1$s Přímé zprávy + Prozkoumat servery Poznámky Toto je začátek vaší konverzace Zpráva #%1$s diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 47c63f84..f4e453ff 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -215,6 +215,7 @@ Möglicherweise fehlen an dieser Stelle Nachrichten. Unbekannte Nachricht Direktnachrichten + Server entdecken Finde deine Community und verbinde dich mit der Welt. Verifizierter Moderationsbot Du kannst in diesem Kanal keine Nachrichten senden. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 40b3b94d..22f266a8 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -216,6 +216,7 @@ Términos de servicio Directivas de comunidad Mensajes Directos + Descubrir servidores Le damos las gracias a los desarrolladores de esos proyectos por hacer disponible su trabajo para todo el mundo. Icono del canal cambiado Descripción del canal cambiada diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 0a196cf3..5324574c 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -197,6 +197,7 @@ Mennään laittamaan sinut kuntoon. Sinulla on kaksivaiheinen todennus päällä että tilisi pystyy turvallisena. Yksityisviestit + Löydä palvelimia Yksityisviesti Siirry alas Kiitoksia että raportoit tämän viestin. Anna syy viestin raportoinnille. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 149d4fd1..329d0a91 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -36,6 +36,7 @@ %1$s Partager l\'audio Messages directs + Découvrir des serveurs Envoyer un message dans %1$s Ajouter une note Pièces jointes envoyées diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 4e216fc5..f093a6de 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -199,6 +199,7 @@ Nisi u kanalu trenutno. %1$s korisnička slika Privatne poruke + Otkrij servere Privatna poruka Tekstni kanal Govorni kanal diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 02426811..93dd663a 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -66,6 +66,7 @@ Kezdésnek válassz ki egy szervert balról. Ha merész vagy, hozz létre egy új szervert! %1$s profilképe Közvetlen üzenetek + Szerverek felfedezése Közvetlen üzenet Szövegcsatorna Beszédcsatorna diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 429e2083..387b02fa 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -199,6 +199,7 @@ Hitam Murni Kode undangan tidak ditentukan. Pesan Langsung + Temukan Server Mari mulai Pedoman Komunitas Kirimkan lampiran diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c2d232f8..4906a0cf 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -243,6 +243,7 @@ Non puoi essere nato prima %1$d. Dovresti essere morto/a adesso. avatar di %1$s Messaggi Diretti + Scopri server Canale Testuale Gruppo Note diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 531b2baa..90864ad0 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -56,6 +56,7 @@ あなたは今チャンネルにいません。 %1$sのアバター ダイレクトメッセージ + サーバーを探す ダイレクトメッセージ テキストチャンネル ボイスチャンネル diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index abcec1a4..6aee22c1 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -210,6 +210,7 @@ Negalite būti gimęs anksčiau nei %1$d. %1$s avataras Tiesioginės žinutės + Atrasti serverius Tiesioginė žinutė Teksto kanalas Balso kanalas diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 85bd1b60..f493246f 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -121,6 +121,7 @@ Jūs nevarat piedzimt pirms %1$d. %1$s avatars Tiešās ziņas + Atklāt serverus Teksta kanāls Balss kanāls Grupa diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index e2edf5db..9f119e49 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -42,4 +42,6 @@ Anderen zullen je vinden, herkennen en vermelden met deze naam. Probeer opnieuw Log uit + Directe berichten + Ontdek servers \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 6498c944..50f8ddb4 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -67,6 +67,7 @@ Wybierz serwer z lewej strony, aby rozpocząć. Jeśli masz ochotę na przygodę, możesz utworzyć nowy serwer. Awatar użytkownika %1$s Wiadomości bezpośrednie + Odkryj serwery Wiadomość bezpośrednia Kanał Tekstowy Kanał głosowy diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index f1e29179..2a1075f1 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -216,6 +216,7 @@ O Material You não é suportado por este dispositivo. Mas se mudares de ideias, podes sempre mudá-lo nas tuas Definições de Utilizador. Mensagens diretas + Descobrir servidores Revolt no Android Trazido a ti com ❤️ pela equipa do Revolt. Versão diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 834ba3de..97de78b7 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -26,6 +26,7 @@ Meniu Nu ești într-un canal acum. Mesaje directe + Descoperă servere Mesaj direct Canal de text Grup diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index cf45ba30..19b18ca5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -216,6 +216,7 @@ Material You (неподдерживаемая) Material You не поддерживается на вашем устройстве. Личные сообщения + Обзор серверов Jenvolt - это место, созданное разработчиками, для всего, что касается приложения Android и многого другого. Поддержка, обратная связь - всё здесь. Возможно, вы сможете опробовать новые функции! 👀 Входящие запросы Проверенный бот модерации diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index cbacfe59..d2157815 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -216,6 +216,7 @@ Ek dosyalar gönder Saf Siyah Direkt Mesajlar + Sunucuları Keşfet Sürüm Yiyecek & İçecek Seyahat & Yerler diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index fb0e7dc1..f6e91c9b 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -216,6 +216,7 @@ ← Назад Невідоме повідомлення Особисті повідомлення + Знайти сервери Revolt побудований за допомогою цих чудових проєктів із відкритим вихідним кодом. Останнє оновлення: %1$s Ми вдячні розробникам цих проєктів за те, що вони зробили свою роботу доступною для світу. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 78738dbb..0896eff1 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -60,6 +60,7 @@ 从左边选择一个服务器就可以开始了。如果您要自己探索,可以创建一个新的服务器。 %1$s 的头像 私人消息 + 发现服务器 私人消息 文字频道 群组 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 62223bbd..51939841 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -226,6 +226,8 @@ %1$s\'s avatar Direct Messages + Discover Servers + Join trending and official groups to be part of something big. Direct Message Text Channel @@ -541,15 +543,17 @@ No app found to handle this link Open - You\'ve been invited to join this server. Would you like to join? - Join - Cancel + You’ve been invited to join + Accept Invite + No Thanks You are already a member of this server. - There was an error + %1$d members + Link No Longer Valid No invite code was specified. - Could not find an invite with the specified code. + This link seems invalid or expired. Reach out for a new invite. You are banned from this server. An unknown error occurred. + Joining… %1$s %1$s @@ -803,4 +807,9 @@ Revolt may block content in certain jurisdictions in response to legislation or legal notices This channel is not available in your region while we review options on legal compliance. Acknowledge + + Error + Retry + No Servers Found + There are no servers available at the moment. Please try again later.