From aa1d90dd85eab48e6e000e3aefb98da15ac1ac4d Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Thu, 17 Jul 2025 13:31:36 -0400 Subject: [PATCH 01/12] Create devcontainer.json Signed-off-by: Alex Yong --- .devcontainer/devcontainer.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..ba843d26 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +{ + "image": "mcr.microsoft.com/devcontainers/universal:2", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/java:1": { + "version": "21", + "jdkDistro": "oracle" + }, + "ghcr.io/nordcominc/devcontainer-features/android-sdk:1": { + "extra_packages": "ndk" + } + }, + "postCreateCommand": "chmod +x gradlew && cp revoltbuild.properties.example revoltbuild.properties && cp sentry.properties.example sentry.properties", + "customizations": { + "vscode": { + "extensions": [ + "redhat.java", + "vscjava.vscode-java-pack", + "kotlin.kotlin" + ] + } + } + } From 1658e9ff2e703c0d8182d6522cfd09304b76e703 Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Thu, 17 Jul 2025 20:43:22 +0000 Subject: [PATCH 02/12] feat: Adding support for long press on server icon should bring up context menu --- .devcontainer/devcontainer.json | 10 +-- app/build.gradle.kts | 12 +++- app/google-services.json.example | 68 +++++++++++++++++++ .../screens/chat/drawer/ChannelSideDrawer.kt | 15 ++-- compliance/licenses/bsd.json | 6 ++ compliance/licenses/mlkit.json | 6 ++ docs/src/content/docs/contributing/setup.mdx | 16 ++++- 7 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 app/google-services.json.example create mode 100644 compliance/licenses/bsd.json create mode 100644 compliance/licenses/mlkit.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ba843d26..1cf96f55 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,16 +1,16 @@ { "image": "mcr.microsoft.com/devcontainers/universal:2", "features": { - "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers-community/features/deno:1": {}, + "ghcr.io/nordcominc/devcontainer-features/android-sdk:1": { + "extra_packages": "ndk;28.1.13356709" + }, "ghcr.io/devcontainers/features/java:1": { "version": "21", "jdkDistro": "oracle" - }, - "ghcr.io/nordcominc/devcontainer-features/android-sdk:1": { - "extra_packages": "ndk" } }, - "postCreateCommand": "chmod +x gradlew && cp revoltbuild.properties.example revoltbuild.properties && cp sentry.properties.example sentry.properties", + "postCreateCommand": "chmod +x gradlew && cp revoltbuild.properties.example revoltbuild.properties && cp sentry.properties.example sentry.properties && git submodule update --init --recursive", "customizations": { "vscode": { "extensions": [ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fe1b9904..e47b3a68 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -89,6 +89,10 @@ android { cppFlags("") } } + + ndk { + abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64") + } } buildTypes { @@ -318,7 +322,7 @@ dependencies { } aboutLibraries { - additionalLicenses += listOf("ofl") + additionalLicenses += listOf("ofl", "bsd", "mlkit") includePlatform = true strictMode = StrictMode.FAIL allowedLicenses += listOf( @@ -329,7 +333,11 @@ aboutLibraries { "BSD-2-Clause", "cmark", "EPL-1.0", - "BSD-3-Clause" + "BSD-3-Clause", + "BSD", + "MLKIT", + "BSD License", + "ML Kit Terms of Service" ) configPath = "compliance" } diff --git a/app/google-services.json.example b/app/google-services.json.example new file mode 100644 index 00000000..2e03d1e6 --- /dev/null +++ b/app/google-services.json.example @@ -0,0 +1,68 @@ +{ + "project_info": { + "project_number": "123456789012", + "project_id": "revolt-android-dev", + "storage_bucket": "revolt-android-dev.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:123456789012:android:abcdef1234567890", + "android_client_info": { + "package_name": "chat.revolt" + } + }, + "oauth_client": [ + { + "client_id": "123456789012-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDummyKeyForDevelopmentOnly1234567890" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "123456789012-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:123456789012:android:abcdef1234567891", + "android_client_info": { + "package_name": "chat.revolt.debug" + } + }, + "oauth_client": [ + { + "client_id": "123456789012-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDummyKeyForDevelopmentOnly1234567890" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "123456789012-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} 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 75442216..0bbeec26 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 @@ -332,12 +332,17 @@ fun ChannelSideDrawer( Modifier .padding(8.dp) .clip(CircleShape) - .clickable { - serverInList.id?.let { srvId -> navigateToServer(srvId) } - scope.launch { - drawerState?.close() + .combinedClickable( + onClick = { + serverInList.id?.let { srvId -> navigateToServer(srvId) } + scope.launch { + drawerState?.close() + } + }, + onLongClick = { + serverInList.id?.let { srvId -> onShowServerContextSheet(srvId) } } - }) { + )) { val icon = serverInList.icon?.id?.let { iconId -> "$REVOLT_FILES/icons/$iconId" } diff --git a/compliance/licenses/bsd.json b/compliance/licenses/bsd.json new file mode 100644 index 00000000..1b718874 --- /dev/null +++ b/compliance/licenses/bsd.json @@ -0,0 +1,6 @@ +{ + "content": "Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.", + "hash": "BSD", + "url": "https://chromium.googlesource.com/libyuv/libyuv/+/refs/heads/main/README.chromium", + "name": "BSD License" +} diff --git a/compliance/licenses/mlkit.json b/compliance/licenses/mlkit.json new file mode 100644 index 00000000..eb4495df --- /dev/null +++ b/compliance/licenses/mlkit.json @@ -0,0 +1,6 @@ +{ + "content": "These terms govern your use of ML Kit and the features you access. By using ML Kit, you agree to these terms.\n\nML Kit enables you to build machine learning features into your mobile apps. ML Kit provides easy-to-use SDKs that bring Google's machine learning expertise to your apps in a powerful yet easy-to-use package.\n\nYour use of ML Kit is subject to the Google APIs Terms of Service and Google's Privacy Policy. You may not use ML Kit if you are a person barred from receiving the software under the laws of the United States or other countries, including the country in which you are resident or from which you use the software.\n\nML Kit APIs are provided under the terms described here. Additional terms may apply to certain ML Kit APIs as described in the documentation for those APIs.", + "hash": "MLKIT", + "url": "https://developers.google.com/ml-kit/terms", + "name": "ML Kit Terms of Service" +} diff --git a/docs/src/content/docs/contributing/setup.mdx b/docs/src/content/docs/contributing/setup.mdx index e69c28a3..2303c5ed 100644 --- a/docs/src/content/docs/contributing/setup.mdx +++ b/docs/src/content/docs/contributing/setup.mdx @@ -137,11 +137,23 @@ import { Tabs, TabItem, Steps } from "@astrojs/starlight/components" You can get these values from the Sentry dashboard. -10. Build the project. +10. Copy the `google-services.json.example` file within the `app` directory to `google-services.json`. + + Firebase services are integrated into the project, so we need a `google-services.json` file for the build to succeed. For development purposes, use the provided example file: + + ```sh + cp app/google-services.json.example app/google-services.json + ``` + + :::note + This is a mock configuration file for development purposes only. In a production environment, you would use a real Firebase project configuration. + ::: + +11. Build the project. You can build the project by clicking on the 'Run' button in Android Studio. If asked, build the `:app` module. -11. **You're all set!** You can now start contributing to Revolt on Android. +12. **You're all set!** You can now start contributing to Revolt on Android. From 6f7f18d8c1bb340df7886d877e28729dcef0880d Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Wed, 6 Aug 2025 16:30:19 +0000 Subject: [PATCH 03/12] Removing unneeded files, updating documentation and devcontainer setup. --- .devcontainer/devcontainer.json | 2 +- compliance/licenses/bsd.json | 6 ------ compliance/licenses/mlkit.json | 6 ------ docs/src/content/docs/contributing/setup.mdx | 2 ++ 4 files changed, 3 insertions(+), 13 deletions(-) delete mode 100644 compliance/licenses/bsd.json delete mode 100644 compliance/licenses/mlkit.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1cf96f55..91f2e3f2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -10,7 +10,7 @@ "jdkDistro": "oracle" } }, - "postCreateCommand": "chmod +x gradlew && cp revoltbuild.properties.example revoltbuild.properties && cp sentry.properties.example sentry.properties && git submodule update --init --recursive", + "postCreateCommand": "chmod +x gradlew && cp revoltbuild.properties.example revoltbuild.properties && cp sentry.properties.example sentry.properties && cp app/google-services.json.example app/google-services.json && git submodule update --init --recursive", "customizations": { "vscode": { "extensions": [ diff --git a/compliance/licenses/bsd.json b/compliance/licenses/bsd.json deleted file mode 100644 index 1b718874..00000000 --- a/compliance/licenses/bsd.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "content": "Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.", - "hash": "BSD", - "url": "https://chromium.googlesource.com/libyuv/libyuv/+/refs/heads/main/README.chromium", - "name": "BSD License" -} diff --git a/compliance/licenses/mlkit.json b/compliance/licenses/mlkit.json deleted file mode 100644 index eb4495df..00000000 --- a/compliance/licenses/mlkit.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "content": "These terms govern your use of ML Kit and the features you access. By using ML Kit, you agree to these terms.\n\nML Kit enables you to build machine learning features into your mobile apps. ML Kit provides easy-to-use SDKs that bring Google's machine learning expertise to your apps in a powerful yet easy-to-use package.\n\nYour use of ML Kit is subject to the Google APIs Terms of Service and Google's Privacy Policy. You may not use ML Kit if you are a person barred from receiving the software under the laws of the United States or other countries, including the country in which you are resident or from which you use the software.\n\nML Kit APIs are provided under the terms described here. Additional terms may apply to certain ML Kit APIs as described in the documentation for those APIs.", - "hash": "MLKIT", - "url": "https://developers.google.com/ml-kit/terms", - "name": "ML Kit Terms of Service" -} diff --git a/docs/src/content/docs/contributing/setup.mdx b/docs/src/content/docs/contributing/setup.mdx index 2303c5ed..88569a16 100644 --- a/docs/src/content/docs/contributing/setup.mdx +++ b/docs/src/content/docs/contributing/setup.mdx @@ -10,6 +10,8 @@ If you want to compile the app for yourself, you can also follow these guideline :::danger It may be tempting to skip some of these steps, but make sure you follow them to ensure that your development environment is set up correctly. + +Note: If you are doing this in Github Codespaces, steps 1-10 will be done for you. ::: import { Tabs, TabItem, Steps } from "@astrojs/starlight/components" From 85ad6b5b14e8faca54773aafa1b5172c53fde9ca Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Wed, 6 Aug 2025 19:06:54 +0000 Subject: [PATCH 04/12] stashing changes --- .../screens/chat/drawer/ChannelSideDrawer.kt | 253 +++++++++++++++++- 1 file changed, 252 insertions(+), 1 deletion(-) 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 a560ca9d..cd358b2b 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,5 +1,6 @@ package chat.revolt.composables.screens.chat.drawer +import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring @@ -37,7 +38,12 @@ 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.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox import androidx.compose.material3.DrawerState +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -46,6 +52,7 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -65,8 +72,11 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -81,6 +91,7 @@ import chat.revolt.api.internals.CategorisedChannelList import chat.revolt.api.internals.ChannelUtils import chat.revolt.api.internals.DirectMessages import chat.revolt.api.internals.FriendRequests +import chat.revolt.api.routes.server.leaveOrDeleteServer import chat.revolt.api.schemas.Category import chat.revolt.api.schemas.Channel import chat.revolt.api.schemas.ChannelType @@ -96,8 +107,10 @@ 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.internals.Platform import chat.revolt.screens.chat.ChatRouterDestination import chat.revolt.screens.chat.LocalIsConnected +import chat.revolt.screens.chat.dialogs.safety.ReportServerDialog import chat.revolt.sheets.ChannelContextSheet import kotlinx.coroutines.launch @@ -183,6 +196,10 @@ fun ChannelSideDrawer( }.sortedBy { it.id })) var channelContextSheetTarget by remember { mutableStateOf(null) } + var serverContextMenuTarget by remember { mutableStateOf(null) } + var showLeaveConfirmation by remember { mutableStateOf(null) } + var leaveSilently by remember { mutableStateOf(false) } + var showReportServerDialog by remember { mutableStateOf(null) } if (channelContextSheetTarget != null) { val channelContextSheetState = rememberModalBottomSheetState() @@ -204,6 +221,96 @@ fun ChannelSideDrawer( } val scope = rememberCoroutineScope() + val clipboardManager = LocalClipboardManager.current + val context = LocalContext.current + + // Leave server confirmation dialog + showLeaveConfirmation?.let { serverId -> + val server = RevoltAPI.serverCache[serverId] + AlertDialog( + onDismissRequest = { + showLeaveConfirmation = null + leaveSilently = false + }, + title = { + Text( + text = stringResource( + id = R.string.server_context_sheet_actions_leave_confirm, + server?.name ?: stringResource(R.string.unknown) + ) + ) + }, + text = { + Column { + Text( + text = stringResource( + id = R.string.server_context_sheet_actions_leave_confirm_eyebrow + ) + ) + Row( + Modifier + .fillMaxWidth() + .padding(start = 0.dp, end = 0.dp, top = 16.dp, bottom = 0.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = leaveSilently, + onCheckedChange = { leaveSilently = it } + ) + Text( + text = stringResource( + id = R.string.server_context_sheet_actions_leave_silently + ), + modifier = Modifier.padding(start = 4.dp) + ) + } + } + }, + confirmButton = { + Button( + onClick = { + scope.launch { + leaveOrDeleteServer(serverId, leaveSilently) + } + showLeaveConfirmation = null + leaveSilently = false + serverContextMenuTarget = null + } + ) { + Text( + text = stringResource( + id = R.string.server_context_sheet_actions_leave_confirm_yes + ) + ) + } + }, + dismissButton = { + TextButton( + onClick = { + showLeaveConfirmation = null + leaveSilently = false + } + ) { + Text( + text = stringResource( + id = R.string.server_context_sheet_actions_leave_confirm_no + ) + ) + } + } + ) + } + + // Report server dialog + showReportServerDialog?.let { serverId -> + ReportServerDialog( + onDismiss = { + showReportServerDialog = null + serverContextMenuTarget = null + }, + serverId = serverId + ) + } Row(modifier.fillMaxSize()) { LazyColumn( @@ -363,7 +470,9 @@ fun ChannelSideDrawer( } }, onLongClick = { - serverInList.id?.let { srvId -> onShowServerContextSheet(srvId) } + serverInList.id?.let { srvId -> + serverContextMenuTarget = srvId + } } )) { val icon = serverInList.icon?.id?.let { iconId -> @@ -388,6 +497,148 @@ fun ChannelSideDrawer( } } + // Server context dropdown menu + DropdownMenu( + expanded = serverContextMenuTarget == serverInList.id, + onDismissRequest = { serverContextMenuTarget = null } + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.server_context_sheet_actions_mark_read)) }, + onClick = { + scope.launch { + serverInList.id?.let { serverId -> + RevoltAPI.unreads.markServerAsRead(serverId, sync = true) + } + } + serverContextMenuTarget = null + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.icn_mark_chat_read_24dp), + contentDescription = null + ) + } + ) + + DropdownMenuItem( + text = { Text("Notification options (TODO)") }, + onClick = { + // TODO: Implement notification options + serverContextMenuTarget = null + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.icn_notification_settings_24dp), + contentDescription = null + ) + }, + trailingIcon = { + Icon( + painter = painterResource(R.drawable.icn_keyboard_arrow_right_24dp), + contentDescription = null + ) + } + ) + + DropdownMenuItem( + text = { Text("Create invite TODO") }, + onClick = { + // TODO: Implement create invite + serverContextMenuTarget = null + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.icn_add_24dp), + contentDescription = null + ) + } + ) + + DropdownMenuItem( + text = { Text("Edit identity TODO") }, + onClick = { + // TODO: Implement edit identity + serverContextMenuTarget = null + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.icn_edit_24dp), + contentDescription = null + ) + } + ) + + DropdownMenuItem( + text = { Text(stringResource(R.string.server_context_sheet_actions_copy_id)) }, + onClick = { + serverInList.id?.let { serverId -> + clipboardManager.setText(AnnotatedString(serverId)) + + if (Platform.needsShowClipboardNotification()) { + Toast.makeText( + context, + context.getString(R.string.server_context_sheet_actions_copy_id_copied), + Toast.LENGTH_SHORT + ).show() + } + } + serverContextMenuTarget = null + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.icn_content_copy_24dp), + contentDescription = null + ) + } + ) + + HorizontalDivider() + + if (serverInList.owner != RevoltAPI.selfId) { + DropdownMenuItem( + text = { + Text( + stringResource(R.string.server_context_sheet_actions_report), + color = MaterialTheme.colorScheme.error + ) + }, + onClick = { + serverInList.id?.let { serverId -> + showReportServerDialog = serverId + } + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.icn_report_24dp), + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + } + ) + + DropdownMenuItem( + text = { + Text( + stringResource(R.string.server_context_sheet_actions_leave), + color = MaterialTheme.colorScheme.error + ) + }, + onClick = { + serverInList.id?.let { serverId -> + showLeaveConfirmation = serverId + } + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.icn_door_open_24dp), + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + } + ) + } + } + Box( Modifier .height(leftIndicatorHeight.value) From 7ffd14e731f4352a2f0003ed718f3fc65d8c30c4 Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Wed, 6 Aug 2025 20:28:15 +0000 Subject: [PATCH 05/12] Saving more changes --- .../screens/chat/drawer/ChannelSideDrawer.kt | 69 +++++++++---------- revite | 1 + 2 files changed, 32 insertions(+), 38 deletions(-) create mode 160000 revite 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 cd358b2b..a1e6a23f 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 @@ -83,6 +83,7 @@ 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.compose.ui.window.Dialog import androidx.navigation.NavController import chat.revolt.R import chat.revolt.api.REVOLT_FILES @@ -110,6 +111,7 @@ import chat.revolt.composables.screens.chat.ChannelIcon import chat.revolt.internals.Platform import chat.revolt.screens.chat.ChatRouterDestination import chat.revolt.screens.chat.LocalIsConnected +import chat.revolt.screens.chat.dialogs.InviteDialog import chat.revolt.screens.chat.dialogs.safety.ReportServerDialog import chat.revolt.sheets.ChannelContextSheet import kotlinx.coroutines.launch @@ -200,6 +202,7 @@ fun ChannelSideDrawer( var showLeaveConfirmation by remember { mutableStateOf(null) } var leaveSilently by remember { mutableStateOf(false) } var showReportServerDialog by remember { mutableStateOf(null) } + var showInviteDialog by remember { mutableStateOf(null) } if (channelContextSheetTarget != null) { val channelContextSheetState = rememberModalBottomSheetState() @@ -312,6 +315,29 @@ fun ChannelSideDrawer( ) } + showInviteDialog?.let { serverId -> + val server = RevoltAPI.serverCache[serverId] + val channelId = server?.channels?.firstOrNull() + if (channelId != null) { + Dialog( + onDismissRequest = { + showInviteDialog = null + } + ) { + InviteDialog( + channelId = channelId, + onDismissRequest = { + showInviteDialog = null + } + ) + } + } else { + // No channels available, dismiss + showInviteDialog = null + serverContextMenuTarget = null + } + } + Row(modifier.fillMaxSize()) { LazyColumn( Modifier.width(64.dp), @@ -497,7 +523,6 @@ fun ChannelSideDrawer( } } - // Server context dropdown menu DropdownMenu( expanded = serverContextMenuTarget == serverInList.id, onDismissRequest = { serverContextMenuTarget = null } @@ -520,30 +545,13 @@ fun ChannelSideDrawer( } ) - DropdownMenuItem( - text = { Text("Notification options (TODO)") }, - onClick = { - // TODO: Implement notification options - serverContextMenuTarget = null - }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_notification_settings_24dp), - contentDescription = null - ) - }, - trailingIcon = { - Icon( - painter = painterResource(R.drawable.icn_keyboard_arrow_right_24dp), - contentDescription = null - ) - } - ) DropdownMenuItem( - text = { Text("Create invite TODO") }, + text = { Text("Create invite") }, onClick = { - // TODO: Implement create invite + serverInList.id?.let { serverId -> + showInviteDialog = serverId + } serverContextMenuTarget = null }, leadingIcon = { @@ -554,20 +562,6 @@ fun ChannelSideDrawer( } ) - DropdownMenuItem( - text = { Text("Edit identity TODO") }, - onClick = { - // TODO: Implement edit identity - serverContextMenuTarget = null - }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_edit_24dp), - contentDescription = null - ) - } - ) - DropdownMenuItem( text = { Text(stringResource(R.string.server_context_sheet_actions_copy_id)) }, onClick = { @@ -592,9 +586,8 @@ fun ChannelSideDrawer( } ) - HorizontalDivider() - if (serverInList.owner != RevoltAPI.selfId) { + HorizontalDivider() DropdownMenuItem( text = { Text( diff --git a/revite b/revite new file mode 160000 index 00000000..92e84309 --- /dev/null +++ b/revite @@ -0,0 +1 @@ +Subproject commit 92e84309ec82b0afc309f74847332bf6241522ea From e2fe8b229dbc3d609b8b2ff36a21995cdad846c6 Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Wed, 6 Aug 2025 20:40:33 +0000 Subject: [PATCH 06/12] chore: cleaning up some code --- .../composables/screens/chat/drawer/ChannelSideDrawer.kt | 3 --- 1 file changed, 3 deletions(-) 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 a1e6a23f..a8ec6198 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 @@ -227,7 +227,6 @@ fun ChannelSideDrawer( val clipboardManager = LocalClipboardManager.current val context = LocalContext.current - // Leave server confirmation dialog showLeaveConfirmation?.let { serverId -> val server = RevoltAPI.serverCache[serverId] AlertDialog( @@ -304,7 +303,6 @@ fun ChannelSideDrawer( ) } - // Report server dialog showReportServerDialog?.let { serverId -> ReportServerDialog( onDismiss = { @@ -332,7 +330,6 @@ fun ChannelSideDrawer( ) } } else { - // No channels available, dismiss showInviteDialog = null serverContextMenuTarget = null } From 976d8c09e8bbe055b439684d9ddb9a62cb7404f6 Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Wed, 6 Aug 2025 20:47:39 +0000 Subject: [PATCH 07/12] chore: removing gitmodule i added in by mistake --- revite | 1 - 1 file changed, 1 deletion(-) delete mode 160000 revite diff --git a/revite b/revite deleted file mode 160000 index 92e84309..00000000 --- a/revite +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 92e84309ec82b0afc309f74847332bf6241522ea From a46a48380844918faa5bca22fecb08a75f5284cc Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Wed, 6 Aug 2025 18:24:26 -0400 Subject: [PATCH 08/12] adding in missing string reference from invite button Signed-off-by: Alex Yong --- .../composables/screens/chat/drawer/ChannelSideDrawer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a8ec6198..ed16b470 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 @@ -544,7 +544,7 @@ fun ChannelSideDrawer( DropdownMenuItem( - text = { Text("Create invite") }, + text = { Text(stringResource(R.string.channel_info_sheet_options_invite)) }, onClick = { serverInList.id?.let { serverId -> showInviteDialog = serverId @@ -1331,4 +1331,4 @@ fun DMOrGroupItem( } } } -} \ No newline at end of file +} From ae21da77119b51702d47007cc4837787c08190fb Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Wed, 6 Aug 2025 22:03:19 -0400 Subject: [PATCH 09/12] adding clarification for deno script in codespaces. Signed-off-by: Alex Yong --- docs/src/content/docs/contributing/setup.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/contributing/setup.mdx b/docs/src/content/docs/contributing/setup.mdx index 88569a16..98b64492 100644 --- a/docs/src/content/docs/contributing/setup.mdx +++ b/docs/src/content/docs/contributing/setup.mdx @@ -11,7 +11,7 @@ If you want to compile the app for yourself, you can also follow these guideline :::danger It may be tempting to skip some of these steps, but make sure you follow them to ensure that your development environment is set up correctly. -Note: If you are doing this in Github Codespaces, steps 1-10 will be done for you. +Note: If you are doing this in Github Codespaces, steps 1-10 (except 7) will be done for you. ::: import { Tabs, TabItem, Steps } from "@astrojs/starlight/components" From 75e94f0a89b26c9fcaee20dce38f6d79dd3ee1da Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Thu, 7 Aug 2025 02:34:16 +0000 Subject: [PATCH 10/12] feat: givingvthe deno scriptvan auto confirm option so it works in devcontainer.json postCreate script. --- .devcontainer/devcontainer.json | 2 +- docs/src/content/docs/contributing/setup.mdx | 2 +- scripts/download_deps.ts | 24 ++++++++++++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 91f2e3f2..7b70f8bf 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -10,7 +10,7 @@ "jdkDistro": "oracle" } }, - "postCreateCommand": "chmod +x gradlew && cp revoltbuild.properties.example revoltbuild.properties && cp sentry.properties.example sentry.properties && cp app/google-services.json.example app/google-services.json && git submodule update --init --recursive", + "postCreateCommand": "chmod +x gradlew && deno run -A scripts/download_deps.ts --yes && cp revoltbuild.properties.example revoltbuild.properties && cp sentry.properties.example sentry.properties && cp app/google-services.json.example app/google-services.json && git submodule update --init --recursive", "customizations": { "vscode": { "extensions": [ diff --git a/docs/src/content/docs/contributing/setup.mdx b/docs/src/content/docs/contributing/setup.mdx index 98b64492..88569a16 100644 --- a/docs/src/content/docs/contributing/setup.mdx +++ b/docs/src/content/docs/contributing/setup.mdx @@ -11,7 +11,7 @@ If you want to compile the app for yourself, you can also follow these guideline :::danger It may be tempting to skip some of these steps, but make sure you follow them to ensure that your development environment is set up correctly. -Note: If you are doing this in Github Codespaces, steps 1-10 (except 7) will be done for you. +Note: If you are doing this in Github Codespaces, steps 1-10 will be done for you. ::: import { Tabs, TabItem, Steps } from "@astrojs/starlight/components" diff --git a/scripts/download_deps.ts b/scripts/download_deps.ts index 6476353b..c9d252ab 100644 --- a/scripts/download_deps.ts +++ b/scripts/download_deps.ts @@ -1,4 +1,14 @@ import { resolve } from "jsr:@std/path" +import { parseArgs } from "jsr:@std/cli/parse-args" + +const args = parseArgs(Deno.args, { + boolean: ["yes", "y", "auto-accept"], + alias: { + "y": "yes" + } +}) + +const autoAccept = args.yes || args["auto-accept"] const outputFolderParent = resolve(Deno.cwd(), "app", "src", "main", "assets") @@ -13,7 +23,7 @@ try { console.error( "Usage: " + "\x1b[35m" + // magenta - "deno run -A scripts/download_deps.ts" + + "deno run -A scripts/download_deps.ts [--yes|-y|--auto-accept]" + "\x1b[0m" + " from the " + "\x1b[1;31;4m" + // bold red underline @@ -299,10 +309,14 @@ for (const dep of deps) { console.log(`- ${dep.file} from ${dep.url}`) } -console.log("Will download the above files.") -if (!confirm("Continue?")) { - console.log("Aborted.") - Deno.exit(0) +if (!autoAccept) { + console.log("Will download the above files.") + if (!confirm("Continue?")) { + console.log("Aborted.") + Deno.exit(0) + } +} else { + console.log("Will download the above files. (auto-accepted)") } const fontsFolder = resolve(outputFolder, "fonts") From bf3c72d1740743711ba8d30bae580fe57c053d72 Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Mon, 11 Aug 2025 13:10:30 -0400 Subject: [PATCH 11/12] feature: Adding support for Github codespaces (#25) * Adding support for codespaces Signed-off-by: Alex Yong * feat: allowing this to be ran in automated contexts Signed-off-by: Alex Yong * Update devcontainer.json Signed-off-by: Alex Yong * feat: Adding in dummy google services json file, and updating documentation * fix: typo :) * Adjusting storage bucket per feedback in Revolt Dev server. --------- Signed-off-by: Alex Yong --- .devcontainer/devcontainer.json | 23 +++++++ app/google-services.json.example | 68 ++++++++++++++++++++ docs/src/content/docs/contributing/setup.mdx | 25 +++++-- scripts/download_deps.ts | 24 +++++-- 4 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 app/google-services.json.example diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..7b70f8bf --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +{ + "image": "mcr.microsoft.com/devcontainers/universal:2", + "features": { + "ghcr.io/devcontainers-community/features/deno:1": {}, + "ghcr.io/nordcominc/devcontainer-features/android-sdk:1": { + "extra_packages": "ndk;28.1.13356709" + }, + "ghcr.io/devcontainers/features/java:1": { + "version": "21", + "jdkDistro": "oracle" + } + }, + "postCreateCommand": "chmod +x gradlew && deno run -A scripts/download_deps.ts --yes && cp revoltbuild.properties.example revoltbuild.properties && cp sentry.properties.example sentry.properties && cp app/google-services.json.example app/google-services.json && git submodule update --init --recursive", + "customizations": { + "vscode": { + "extensions": [ + "redhat.java", + "vscjava.vscode-java-pack", + "kotlin.kotlin" + ] + } + } + } diff --git a/app/google-services.json.example b/app/google-services.json.example new file mode 100644 index 00000000..997e884c --- /dev/null +++ b/app/google-services.json.example @@ -0,0 +1,68 @@ +{ + "project_info": { + "project_number": "123456789012", + "project_id": "revolt-android-dev", + "storage_bucket": "revolt-chat.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:123456789012:android:abcdef1234567890", + "android_client_info": { + "package_name": "chat.revolt" + } + }, + "oauth_client": [ + { + "client_id": "123456789012-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDummyKeyForDevelopmentOnly1234567890" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "123456789012-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:123456789012:android:abcdef1234567891", + "android_client_info": { + "package_name": "chat.revolt.debug" + } + }, + "oauth_client": [ + { + "client_id": "123456789012-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDummyKeyForDevelopmentOnly1234567890" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "123456789012-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/docs/src/content/docs/contributing/setup.mdx b/docs/src/content/docs/contributing/setup.mdx index e69c28a3..6bfb440e 100644 --- a/docs/src/content/docs/contributing/setup.mdx +++ b/docs/src/content/docs/contributing/setup.mdx @@ -6,10 +6,12 @@ template: doc This page contains the guidelines for setting up your development environment for Revolt on Android. These guidelines are important to ensure that your development environment is set up correctly and you can start contributing to the project. -If you want to compile the app for yourself, you can also follow these guidelines, however you may not need Android Studio and it may be possible to build the app using the command line, out of the scope of this guide. +If you want to compile the app yourself, you can follow these guidelines. You may not need Android Studio, as it’s possible to build the app from the command line. GitHub Codespaces automates most of the process for you if you want to go that route and this guide covers those steps, but won’t go into detail on other command-line methods. :::danger It may be tempting to skip some of these steps, but make sure you follow them to ensure that your development environment is set up correctly. + +Note: If you are doing this in Github Codespaces, steps 1-10 will be done for you. Also note that while small codespace instances can build this app, it's recommend to use 8-core or above for best results. ::: import { Tabs, TabItem, Steps } from "@astrojs/starlight/components" @@ -137,11 +139,26 @@ import { Tabs, TabItem, Steps } from "@astrojs/starlight/components" You can get these values from the Sentry dashboard. -10. Build the project. +10. Copy the `google-services.json.example` file within the `app` directory to `google-services.json`. + + Firebase services are integrated into the project, so we need a `google-services.json` file for the build to succeed. For development purposes, use the provided example file: + + ```sh + cp app/google-services.json.example app/google-services.json + ``` + + :::note + This is a mock configuration file for development purposes only. In a production environment, you would use a real Firebase project configuration. + ::: + +11. Build the project. You can build the project by clicking on the 'Run' button in Android Studio. If asked, build the `:app` module. -11. **You're all set!** You can now start contributing to Revolt on Android. + If building within Github Codespaces, you can build a fresh debug apk by running `./gradlew assembledebug --no-daemon` from the root of the project. + Upon completion, the apk will be within `app/build/outputs/apk/debug/` - +12. **You're all set!** You can now start contributing to Revolt on Android. + + \ No newline at end of file diff --git a/scripts/download_deps.ts b/scripts/download_deps.ts index 6476353b..c9d252ab 100644 --- a/scripts/download_deps.ts +++ b/scripts/download_deps.ts @@ -1,4 +1,14 @@ import { resolve } from "jsr:@std/path" +import { parseArgs } from "jsr:@std/cli/parse-args" + +const args = parseArgs(Deno.args, { + boolean: ["yes", "y", "auto-accept"], + alias: { + "y": "yes" + } +}) + +const autoAccept = args.yes || args["auto-accept"] const outputFolderParent = resolve(Deno.cwd(), "app", "src", "main", "assets") @@ -13,7 +23,7 @@ try { console.error( "Usage: " + "\x1b[35m" + // magenta - "deno run -A scripts/download_deps.ts" + + "deno run -A scripts/download_deps.ts [--yes|-y|--auto-accept]" + "\x1b[0m" + " from the " + "\x1b[1;31;4m" + // bold red underline @@ -299,10 +309,14 @@ for (const dep of deps) { console.log(`- ${dep.file} from ${dep.url}`) } -console.log("Will download the above files.") -if (!confirm("Continue?")) { - console.log("Aborted.") - Deno.exit(0) +if (!autoAccept) { + console.log("Will download the above files.") + if (!confirm("Continue?")) { + console.log("Aborted.") + Deno.exit(0) + } +} else { + console.log("Will download the above files. (auto-accepted)") } const fontsFolder = resolve(outputFolder, "fonts") From e66468830ce4528030ce97bbea2b71f70f855d91 Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Wed, 13 Aug 2025 15:16:01 +0000 Subject: [PATCH 12/12] Adjusting UI per feedback on official Revolt server. long press will now show the server context menu as a bottom sheet. --- app/google-services.json.example | 2 +- .../screens/chat/drawer/ChannelSideDrawer.kt | 267 +++--------------- 2 files changed, 34 insertions(+), 235 deletions(-) diff --git a/app/google-services.json.example b/app/google-services.json.example index 0204e396..997e884c 100644 --- a/app/google-services.json.example +++ b/app/google-services.json.example @@ -65,4 +65,4 @@ } ], "configuration_version": "1" -} +} \ 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 ed16b470..6e05acff 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,6 +1,5 @@ package chat.revolt.composables.screens.chat.drawer -import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring @@ -38,12 +37,7 @@ 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.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Checkbox import androidx.compose.material3.DrawerState -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -52,7 +46,6 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -72,18 +65,14 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString 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.compose.ui.window.Dialog import androidx.navigation.NavController import chat.revolt.R import chat.revolt.api.REVOLT_FILES @@ -92,7 +81,6 @@ import chat.revolt.api.internals.CategorisedChannelList import chat.revolt.api.internals.ChannelUtils import chat.revolt.api.internals.DirectMessages import chat.revolt.api.internals.FriendRequests -import chat.revolt.api.routes.server.leaveOrDeleteServer import chat.revolt.api.schemas.Category import chat.revolt.api.schemas.Channel import chat.revolt.api.schemas.ChannelType @@ -108,12 +96,11 @@ 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.internals.Platform import chat.revolt.screens.chat.ChatRouterDestination import chat.revolt.screens.chat.LocalIsConnected -import chat.revolt.screens.chat.dialogs.InviteDialog import chat.revolt.screens.chat.dialogs.safety.ReportServerDialog import chat.revolt.sheets.ChannelContextSheet +import chat.revolt.sheets.ServerContextSheet import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @@ -198,11 +185,10 @@ fun ChannelSideDrawer( }.sortedBy { it.id })) var channelContextSheetTarget by remember { mutableStateOf(null) } - var serverContextMenuTarget by remember { mutableStateOf(null) } - var showLeaveConfirmation by remember { mutableStateOf(null) } - var leaveSilently by remember { mutableStateOf(false) } - var showReportServerDialog by remember { mutableStateOf(null) } - var showInviteDialog by remember { mutableStateOf(null) } + var serverContextSheetTarget by remember { mutableStateOf(null) } + + var showReportServer by remember { mutableStateOf(false) } + var reportServerTarget by remember { mutableStateOf("") } if (channelContextSheetTarget != null) { val channelContextSheetState = rememberModalBottomSheetState() @@ -223,118 +209,31 @@ fun ChannelSideDrawer( } } - val scope = rememberCoroutineScope() - val clipboardManager = LocalClipboardManager.current - val context = LocalContext.current + if (serverContextSheetTarget != null) { + val serverContextSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - showLeaveConfirmation?.let { serverId -> - val server = RevoltAPI.serverCache[serverId] - AlertDialog( + ModalBottomSheet( + sheetState = serverContextSheetState, onDismissRequest = { - showLeaveConfirmation = null - leaveSilently = false - }, - title = { - Text( - text = stringResource( - id = R.string.server_context_sheet_actions_leave_confirm, - server?.name ?: stringResource(R.string.unknown) - ) - ) - }, - text = { - Column { - Text( - text = stringResource( - id = R.string.server_context_sheet_actions_leave_confirm_eyebrow - ) - ) - Row( - Modifier - .fillMaxWidth() - .padding(start = 0.dp, end = 0.dp, top = 16.dp, bottom = 0.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = leaveSilently, - onCheckedChange = { leaveSilently = it } - ) - Text( - text = stringResource( - id = R.string.server_context_sheet_actions_leave_silently - ), - modifier = Modifier.padding(start = 4.dp) - ) - } - } - }, - confirmButton = { - Button( - onClick = { - scope.launch { - leaveOrDeleteServer(serverId, leaveSilently) - } - showLeaveConfirmation = null - leaveSilently = false - serverContextMenuTarget = null - } - ) { - Text( - text = stringResource( - id = R.string.server_context_sheet_actions_leave_confirm_yes - ) - ) - } - }, - dismissButton = { - TextButton( - onClick = { - showLeaveConfirmation = null - leaveSilently = false - } - ) { - Text( - text = stringResource( - id = R.string.server_context_sheet_actions_leave_confirm_no - ) - ) - } + serverContextSheetTarget = null } - ) - } - - showReportServerDialog?.let { serverId -> - ReportServerDialog( - onDismiss = { - showReportServerDialog = null - serverContextMenuTarget = null - }, - serverId = serverId - ) - } - - showInviteDialog?.let { serverId -> - val server = RevoltAPI.serverCache[serverId] - val channelId = server?.channels?.firstOrNull() - if (channelId != null) { - Dialog( - onDismissRequest = { - showInviteDialog = null + ) { + ServerContextSheet( + serverId = serverContextSheetTarget!!, + onReportServer = { + reportServerTarget = serverContextSheetTarget!! + showReportServer = true + }, + onHideSheet = { + serverContextSheetState.hide() + serverContextSheetTarget = null } - ) { - InviteDialog( - channelId = channelId, - onDismissRequest = { - showInviteDialog = null - } - ) - } - } else { - showInviteDialog = null - serverContextMenuTarget = null + ) } } + val scope = rememberCoroutineScope() + Row(modifier.fillMaxSize()) { LazyColumn( Modifier.width(64.dp), @@ -494,7 +393,7 @@ fun ChannelSideDrawer( }, onLongClick = { serverInList.id?.let { srvId -> - serverContextMenuTarget = srvId + serverContextSheetTarget = srvId } } )) { @@ -520,115 +419,6 @@ fun ChannelSideDrawer( } } - DropdownMenu( - expanded = serverContextMenuTarget == serverInList.id, - onDismissRequest = { serverContextMenuTarget = null } - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.server_context_sheet_actions_mark_read)) }, - onClick = { - scope.launch { - serverInList.id?.let { serverId -> - RevoltAPI.unreads.markServerAsRead(serverId, sync = true) - } - } - serverContextMenuTarget = null - }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_mark_chat_read_24dp), - contentDescription = null - ) - } - ) - - - DropdownMenuItem( - text = { Text(stringResource(R.string.channel_info_sheet_options_invite)) }, - onClick = { - serverInList.id?.let { serverId -> - showInviteDialog = serverId - } - serverContextMenuTarget = null - }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_add_24dp), - contentDescription = null - ) - } - ) - - DropdownMenuItem( - text = { Text(stringResource(R.string.server_context_sheet_actions_copy_id)) }, - onClick = { - serverInList.id?.let { serverId -> - clipboardManager.setText(AnnotatedString(serverId)) - - if (Platform.needsShowClipboardNotification()) { - Toast.makeText( - context, - context.getString(R.string.server_context_sheet_actions_copy_id_copied), - Toast.LENGTH_SHORT - ).show() - } - } - serverContextMenuTarget = null - }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_content_copy_24dp), - contentDescription = null - ) - } - ) - - if (serverInList.owner != RevoltAPI.selfId) { - HorizontalDivider() - DropdownMenuItem( - text = { - Text( - stringResource(R.string.server_context_sheet_actions_report), - color = MaterialTheme.colorScheme.error - ) - }, - onClick = { - serverInList.id?.let { serverId -> - showReportServerDialog = serverId - } - }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_report_24dp), - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - } - ) - - DropdownMenuItem( - text = { - Text( - stringResource(R.string.server_context_sheet_actions_leave), - color = MaterialTheme.colorScheme.error - ) - }, - onClick = { - serverInList.id?.let { serverId -> - showLeaveConfirmation = serverId - } - }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_door_open_24dp), - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - } - ) - } - } - Box( Modifier .height(leftIndicatorHeight.value) @@ -840,6 +630,15 @@ fun ChannelSideDrawer( } } } + + if (showReportServer) { + ReportServerDialog( + onDismiss = { + showReportServer = false + }, + serverId = reportServerTarget + ) + } } @Composable