From adfaa04a719e2b04a3c0c9c1abb972ffc60b6363 Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Mon, 11 Aug 2025 13:10:30 -0400 Subject: [PATCH 01/13] feat: add support for GH codespaces (#25) * feat: support for codespaces Signed-off-by: Alex Yong * feat: allowing this to be ran in automated contexts Signed-off-by: Alex Yong * chore: update devcontainer.json Signed-off-by: Alex Yong * feat: Adding in dummy google services json file, and updating documentation * fix: typo * fix: adjust storage bucket --------- 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 84605d21f8a1539f1dab2bf4a7fe2baeca340be6 Mon Sep 17 00:00:00 2001 From: Infi Date: Wed, 20 Aug 2025 11:12:17 +0200 Subject: [PATCH 02/13] chore: bump android sdk to 36 Signed-off-by: Infi --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a4ca9686..a807dbcf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -73,12 +73,12 @@ fun buildproperty(propertyName: String, fallbackEnv: String? = null): String? { } android { - compileSdk = 35 + compileSdk = 36 defaultConfig { applicationId = "chat.revolt" minSdk = 24 - targetSdk = 35 + targetSdk = 36 versionCode = Integer.parseInt("001_003_206".replace("_", ""), 10) versionName = "1.3.6b" From 6ea094e1b5fc4210c51c2c3d719767ccd4b29b5a Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 21 Aug 2025 19:35:36 +0200 Subject: [PATCH 03/13] feat: apply vm strict mode log penalty, may be adjusted as needed Signed-off-by: Infi --- app/src/main/java/chat/revolt/RevoltApplication.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/chat/revolt/RevoltApplication.kt b/app/src/main/java/chat/revolt/RevoltApplication.kt index e0999f8e..3d7af9e1 100644 --- a/app/src/main/java/chat/revolt/RevoltApplication.kt +++ b/app/src/main/java/chat/revolt/RevoltApplication.kt @@ -1,6 +1,7 @@ package chat.revolt import android.app.Application +import android.os.StrictMode import com.google.android.material.color.DynamicColors import dagger.hilt.android.HiltAndroidApp import logcat.AndroidLogcatLogger @@ -15,6 +16,16 @@ class RevoltApplication : Application() { override fun onCreate() { super.onCreate() AndroidLogcatLogger.installOnDebuggableApp(this, minPriority = LogPriority.VERBOSE) + + StrictMode.setVmPolicy( + StrictMode.VmPolicy + .Builder() + .apply { + detectAll() + penaltyLog() + } + .build() + ) } init { From e4d58120aafb77379c2320c7c316f52a510aa2f0 Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 21 Aug 2025 19:37:46 +0200 Subject: [PATCH 04/13] fix: only use vm strict mode in debug Signed-off-by: Infi --- .../java/chat/revolt/RevoltApplication.kt | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/chat/revolt/RevoltApplication.kt b/app/src/main/java/chat/revolt/RevoltApplication.kt index 3d7af9e1..a9936361 100644 --- a/app/src/main/java/chat/revolt/RevoltApplication.kt +++ b/app/src/main/java/chat/revolt/RevoltApplication.kt @@ -17,15 +17,20 @@ class RevoltApplication : Application() { super.onCreate() AndroidLogcatLogger.installOnDebuggableApp(this, minPriority = LogPriority.VERBOSE) - StrictMode.setVmPolicy( - StrictMode.VmPolicy - .Builder() - .apply { - detectAll() - penaltyLog() - } - .build() - ) + if (BuildConfig.DEBUG) { + // Enable strict mode primarily to catch non-API usage, although we detect all + // violations for our reference. + // https://developer.android.com/reference/android/os/StrictMode + StrictMode.setVmPolicy( + StrictMode.VmPolicy + .Builder() + .apply { + detectAll() + penaltyLog() + } + .build() + ) + } } init { From 9cd738fdce4670805472ba2d50bcbcbc9337f740 Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 21 Aug 2025 19:43:25 +0200 Subject: [PATCH 05/13] fix: use proper name Signed-off-by: Infi --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7695422e..4aff6a52 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,7 +85,7 @@ Enter your password to continue. About - Revolt on Android + Revolt for Android Brought to you with ❤️ by the Revolt team. Version Details From 2e5a3f32da2dae1ec264c1cd7559227cdca152a2 Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 21 Aug 2025 19:43:30 +0200 Subject: [PATCH 06/13] docs: update readme Signed-off-by: Infi --- README.md | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7dd0e77a..cf1f5926 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,12 @@ -# Revolt on Android +# Revolt for Android ## Description -This is the official Android app for the [Revolt](https://revolt.chat) chat platform. +Official [Revolt](https://revolt.chat) Android app + The codebase includes the app itself, as well as an internal library for interacting with the Revolt -API. - -| Module | Package | Description | -|--------|---------------|----------------------| -| `:app` | `chat.revolt` | The main app module. | - -The API library is part of the `app` module, and is not intended to be used as a standalone library, -as it makes liberal use of Android-specific APIs for reactivity. - -The app is written in Kotlin, and uses -the [Jetpack Compose](https://developer.android.com/jetpack/compose) UI toolkit, the current state -of the art for Android UI development. +API. The app is written in Kotlin, and wholly +uses [Jetpack Compose](https://developer.android.com/jetpack/compose). ## Stack @@ -29,9 +20,10 @@ of the art for Android UI development. ## Resources -### Revolt on Android +### Revolt for Android -- [Revolt on Android Technical Documentation](https://revoltchat.github.io/android/) +- [Roadmap](https://op.revolt.wtf/projects/revolt-for-android/work_packages) +- [Revolt for Android Technical Documentation](https://revoltchat.github.io/android/) - [Android-specific Contribution Guide](https://revoltchat.github.io/android/contributing/guidelines/) —**read carefully before contributing!** @@ -39,8 +31,9 @@ of the art for Android UI development. - [Revolt Project Board](https://github.com/revoltchat/revolt/discussions) (Submit feature requests here) -- [Revolt Testers Server](https://app.revolt.chat/invite/Testers) -- [General Revolt Contribution Guide](https://developers.revolt.chat/contributing) +- [Revolt Development Server](https://app.revolt.chat/invite/API) +- [Revolt Server](https://app.revolt.chat/invite/Testers) +- [General Revolt Contribution Guide](https://developers.revolt.chat/contrib.html) ## Quick Start From c25b398c28d3a67f955287afe76fc41f8bfa3870 Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 21 Aug 2025 19:45:08 +0200 Subject: [PATCH 07/13] docs: remove outdated documentation Signed-off-by: Infi --- docs/astro.config.mjs | 15 +---- .../content/docs/beta/availability-regions.md | 65 ------------------- .../docs/beta/joining-the-public-beta-test.md | 20 ------ 3 files changed, 1 insertion(+), 99 deletions(-) delete mode 100644 docs/src/content/docs/beta/availability-regions.md delete mode 100644 docs/src/content/docs/beta/joining-the-public-beta-test.md diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 9d925410..1f6d2134 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -26,20 +26,7 @@ export default defineConfig({ link: "/contributing/setup", } ], - }, - { - label: "Beta Test", - items: [ - { - label: "Geographic Availability", - link: "/beta/availability-regions", - }, - { - label: "Join the Beta Test", - link: "/beta/joining-the-public-beta-test", - } - ], - }, + } /* { label: "Reference", autogenerate: { directory: "reference" }, diff --git a/docs/src/content/docs/beta/availability-regions.md b/docs/src/content/docs/beta/availability-regions.md deleted file mode 100644 index 11b658b4..00000000 --- a/docs/src/content/docs/beta/availability-regions.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Geographic Availability of the Beta Test -description: Find out where you can join the beta test for Revolt on Android. -template: doc ---- - -The beta test for Revolt on Android is available for testers in the following jurisdictions: - -- Armenia -- Australia -- Austria -- Azerbaijan -- Belgium -- Brazil -- Bulgaria -- Canada -- Croatia -- Cyprus -- Czechia -- Denmark (Kingdom of) -- Dominican Republic -- Estonia -- Finland (including Åland) -- France (including overseas départements and other outre-mer territories) -- Georgia -- Germany -- Gibraltar -- Greece -- Hungary -- Iceland -- Ireland -- Israel -- Italy -- Japan -- Latvia -- Liechtenstein -- Lithuania -- Luxembourg -- Malaysia -- Malta -- Moldova -- Monaco -- Netherlands -- New Zealand -- Norway (Kingdom of) -- Poland -- Portugal -- Romania -- San Marino -- Slovakia -- Slovenia -- South Africa -- South Korea -- Spain -- Sweden -- Switzerland -- Türkiye -- Ukraine -- United Kingdom -- United States (including Territories of the U.S.) -- Vatican City - -## My region isn't listed! - -While we don't offer the beta test in *all* regions of the world, we may and most likely will add regions that are not listed upon request. Please request the addition on [Jenvolt](https://rvlt.gg/jen) (if you beta test, you are probably already in the server!). \ No newline at end of file diff --git a/docs/src/content/docs/beta/joining-the-public-beta-test.md b/docs/src/content/docs/beta/joining-the-public-beta-test.md deleted file mode 100644 index 75754a0b..00000000 --- a/docs/src/content/docs/beta/joining-the-public-beta-test.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Joining the Public Beta Test -description: Learn how to join the public beta test for Revolt for Android. -template: doc ---- - -The public beta test for Revolt for Android is available to anyone with an Android device -in [those jurisdictions](https://revoltchat.github.io/android/beta/availability-regions/) where the -beta test is available. The beta test is open to everyone, and you can join the beta test by -following these steps: - -1. [Join the Revolt for Android Alpha Track on Google Groups](https://groups.google.com/g/revolt-android-alpha-track). -2. [Opt-in to the Revolt for Android Beta Test on Google Play](https://play.google.com/apps/testing/chat.revolt). -3. [Download Revolt for Android from Google Play](https://play.google.com/store/apps/details?id=chat.revolt). - -You're all set! You can now use Revolt on Android and help us test new features and improvements. - -If you encounter any issues or have any feedback, please let us know -by [reporting an issue](https://androidtasks.starlightjen.com/) -or [joining the Revolt Developers server](https://rvlt.gg/api). \ No newline at end of file From cb2987b8b965eb2dbda0f3308ed6ffa5aceabce3 Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 21 Aug 2025 19:47:14 +0200 Subject: [PATCH 08/13] docs: add google play badge on readme Signed-off-by: Infi --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cf1f5926..ee9a3742 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ ## Description -Official [Revolt](https://revolt.chat) Android app +Official [Revolt](https://revolt.chat) Android app. + +![Get it on Google Play](https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png) + +Google Play is a trademark of Google LLC. The codebase includes the app itself, as well as an internal library for interacting with the Revolt API. The app is written in Kotlin, and wholly From a0f170f9f468bfb0f77cfe2d13e63cf9378cc0e3 Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 21 Aug 2025 19:48:20 +0200 Subject: [PATCH 09/13] docs: better google play badge sizing Signed-off-by: Infi --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ee9a3742..02a91c26 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,10 @@ Official [Revolt](https://revolt.chat) Android app. -![Get it on Google Play](https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png) - -Google Play is a trademark of Google LLC. +
+ Get it on Google Play + Google Play is a trademark of Google LLC. +
The codebase includes the app itself, as well as an internal library for interacting with the Revolt API. The app is written in Kotlin, and wholly From c13b73ab3aefa33ed72e1566de88dd925c0a0f32 Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 21 Aug 2025 19:48:58 +0200 Subject: [PATCH 10/13] docs: even better google play badge sizing Signed-off-by: Infi --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 02a91c26..926fd4e7 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,11 @@ Official [Revolt](https://revolt.chat) Android app. -
+
Get it on Google Play Google Play is a trademark of Google LLC. -
+ + The codebase includes the app itself, as well as an internal library for interacting with the Revolt API. The app is written in Kotlin, and wholly From fd829deade8350c8d709274eb90fa1eeeeadf26e Mon Sep 17 00:00:00 2001 From: infi Date: Thu, 21 Aug 2025 19:51:45 +0200 Subject: [PATCH 11/13] docs: tweak readme even further Signed-off-by: infi --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 926fd4e7..6929b8ec 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ -# Revolt for Android +
+

Revolt for Android

+

Official Revolt Android app.

+

+
+ Get it on Google Play +
+
+ Google Play is a trademark of Google LLC. +


+
## Description -Official [Revolt](https://revolt.chat) Android app. - -
- Get it on Google Play - Google Play is a trademark of Google LLC. -
- - The codebase includes the app itself, as well as an internal library for interacting with the Revolt API. The app is written in Kotlin, and wholly uses [Jetpack Compose](https://developer.android.com/jetpack/compose). @@ -47,4 +49,4 @@ Open the project in Android Studio. You can then run the app on an emulator or a running the `app` module. In-depth setup instructions can be found -at [Setting up your Development Environment](https://revoltchat.github.io/android/contributing/setup/) \ No newline at end of file +at [Setting up your Development Environment](https://revoltchat.github.io/android/contributing/setup/) From f8c9127d152bfbfd62bd8d7bd7004fa552b3f023 Mon Sep 17 00:00:00 2001 From: Infi Date: Fri, 22 Aug 2025 00:41:40 +0200 Subject: [PATCH 12/13] feat: set the stage for the third and final markdown library Signed-off-by: Infi --- .gitignore | 4 + .../chat/revolt/api/settings/Experiments.kt | 10 ++ .../chat/revolt/api/settings/FeatureFlags.kt | 23 +++ .../java/chat/revolt/ndk/FinalMarkdown.kt | 14 ++ .../java/chat/revolt/ndk/NativeLibraries.kt | 3 + .../settings/ExperimentsSettingsScreen.kt | 137 +++++++++++++++--- app/src/main/jniLibs/.gitkeep | 0 docs/src/content/docs/contributing/setup.mdx | 12 ++ scripts/download_deps.ts | 83 ++++++++++- 9 files changed, 263 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/chat/revolt/ndk/FinalMarkdown.kt create mode 100644 app/src/main/jniLibs/.gitkeep diff --git a/.gitignore b/.gitignore index 7f75bc93..8033a71f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ revoltbuild.properties sentry.properties /.kotlin/sessions app/src/main/assets/embedded +/app/src/main/jniLibs/arm64-v8a/ +/app/src/main/jniLibs/armeabi-v7a/ +/app/src/main/jniLibs/x86/ +/app/src/main/jniLibs/x86_64/ diff --git a/app/src/main/java/chat/revolt/api/settings/Experiments.kt b/app/src/main/java/chat/revolt/api/settings/Experiments.kt index 818d997e..41edc140 100644 --- a/app/src/main/java/chat/revolt/api/settings/Experiments.kt +++ b/app/src/main/java/chat/revolt/api/settings/Experiments.kt @@ -29,6 +29,7 @@ object Experiments { val useKotlinBasedMarkdownRenderer = ExperimentInstance(false) val usePolar = ExperimentInstance(false) val enableServerIdentityOptions = ExperimentInstance(false) + val useFinalMarkdownRenderer = ExperimentInstance(false) suspend fun hydrateWithKv() { val kvStorage = KVStorage(RevoltApplication.instance) @@ -48,5 +49,14 @@ object Experiments { enableServerIdentityOptions.setEnabled( kvStorage.getBoolean("exp/enableServerIdentityOptions") == true ) + useFinalMarkdownRenderer.setEnabled( + kvStorage.getBoolean("exp/useFinalMarkdownRenderer") == true + ) + + if (useFinalMarkdownRenderer.isEnabled && useKotlinBasedMarkdownRenderer.isEnabled) { + // if jbm and fm are enabled, fm takes precedence. this should not be possible in practice + useKotlinBasedMarkdownRenderer.setEnabled(false) + kvStorage.set("exp/useKotlinBasedMarkdownRenderer", false) + } } } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/settings/FeatureFlags.kt b/app/src/main/java/chat/revolt/api/settings/FeatureFlags.kt index f7dee8c9..28dfac83 100644 --- a/app/src/main/java/chat/revolt/api/settings/FeatureFlags.kt +++ b/app/src/main/java/chat/revolt/api/settings/FeatureFlags.kt @@ -56,6 +56,19 @@ sealed class VoiceChannels2_0Variates { object Disabled : VoiceChannels2_0Variates() } +@FeatureFlag("FinalMarkdown") +sealed class FinalMarkdownVariates { + @Treatment( + "Enable the new FinalMarkdown library for all users" + ) + object Enabled : FinalMarkdownVariates() + + @Treatment( + "Disable the new FinalMarkdown library for all users" + ) + object Disabled : FinalMarkdownVariates() +} + object FeatureFlags { @FeatureFlag("LabsAccessControl") var labsAccessControl by mutableStateOf( @@ -101,4 +114,14 @@ object FeatureFlags { is VoiceChannels2_0Variates.Enabled -> true is VoiceChannels2_0Variates.Disabled -> false } + + @FeatureFlag("FinalMarkdown") + var finalMarkdown by mutableStateOf( + FinalMarkdownVariates.Disabled + ) + val finalMarkdownGranted: Boolean + get() = when (finalMarkdown) { + is FinalMarkdownVariates.Enabled -> true + is FinalMarkdownVariates.Disabled -> false + } } diff --git a/app/src/main/java/chat/revolt/ndk/FinalMarkdown.kt b/app/src/main/java/chat/revolt/ndk/FinalMarkdown.kt new file mode 100644 index 00000000..e80247dc --- /dev/null +++ b/app/src/main/java/chat/revolt/ndk/FinalMarkdown.kt @@ -0,0 +1,14 @@ +package chat.revolt.ndk + +import kotlinx.serialization.Serializable + +@Serializable +data class FinalMarkdownNodeTest( + val test: Int, +) + +@Suppress("KotlinJniMissingFunction") +object FinalMarkdown { + external fun init() + external fun process(input: String): FinalMarkdownNodeTest +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/ndk/NativeLibraries.kt b/app/src/main/java/chat/revolt/ndk/NativeLibraries.kt index 10af43e7..e8bc19dd 100644 --- a/app/src/main/java/chat/revolt/ndk/NativeLibraries.kt +++ b/app/src/main/java/chat/revolt/ndk/NativeLibraries.kt @@ -3,12 +3,15 @@ package chat.revolt.ndk annotation class NativeLibrary(val name: String) { companion object { const val LIB_NAME_NATIVE_MARKDOWN = "stendal" + const val LIB_NAME_NATIVE_MARKDOWN_V2 = "finalmarkdown" } } object NativeLibraries { fun init() { System.loadLibrary(NativeLibrary.LIB_NAME_NATIVE_MARKDOWN) + System.loadLibrary(NativeLibrary.LIB_NAME_NATIVE_MARKDOWN_V2) Stendal.init() + FinalMarkdown.init() } } diff --git a/app/src/main/java/chat/revolt/screens/settings/ExperimentsSettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/ExperimentsSettingsScreen.kt index 5b931589..fce5374f 100644 --- a/app/src/main/java/chat/revolt/screens/settings/ExperimentsSettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/ExperimentsSettingsScreen.kt @@ -2,21 +2,36 @@ package chat.revolt.screens.settings import android.content.Context import android.content.Intent +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ListItem import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.ToggleButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel @@ -24,18 +39,35 @@ import androidx.navigation.NavController import chat.revolt.BuildConfig import chat.revolt.RevoltApplication import chat.revolt.api.settings.Experiments +import chat.revolt.api.settings.FeatureFlags import chat.revolt.api.settings.LoadedSettings import chat.revolt.persistence.KVStorage import chat.revolt.settings.dsl.SettingsPage import chat.revolt.settings.dsl.SubcategoryContentInsets import kotlinx.coroutines.launch +enum class MarkdownRenderer { + Stendal, JetBrains, FinalMarkdown +} + class ExperimentsSettingsScreenViewModel : ViewModel() { private val kv = KVStorage(RevoltApplication.instance) fun init() { viewModelScope.launch { - useKotlinMdRendererChecked.value = Experiments.useKotlinBasedMarkdownRenderer.isEnabled + when { + Experiments.useKotlinBasedMarkdownRenderer.isEnabled -> { + mdRenderer.value = MarkdownRenderer.JetBrains + } + + Experiments.useFinalMarkdownRenderer.isEnabled -> { + mdRenderer.value = MarkdownRenderer.FinalMarkdown + } + + else -> { + mdRenderer.value = MarkdownRenderer.Stendal + } + } usePolarChecked.value = Experiments.usePolar.isEnabled enableServerIdentityOptionsChecked.value = Experiments.enableServerIdentityOptions.isEnabled @@ -66,13 +98,33 @@ class ExperimentsSettingsScreenViewModel : ViewModel() { } } - val useKotlinMdRendererChecked = mutableStateOf(false) + val mdRenderer = mutableStateOf(MarkdownRenderer.Stendal) - fun setUseKotlinMdRendererChecked(value: Boolean) { + fun setMdRenderer(value: MarkdownRenderer) { viewModelScope.launch { - kv.set("exp/useKotlinBasedMarkdownRenderer", value) - Experiments.useKotlinBasedMarkdownRenderer.setEnabled(value) - useKotlinMdRendererChecked.value = value + when (value) { + MarkdownRenderer.Stendal -> { + kv.set("exp/useKotlinBasedMarkdownRenderer", false) + Experiments.useKotlinBasedMarkdownRenderer.setEnabled(false) + kv.set("exp/useFinalMarkdownRenderer", false) + Experiments.useFinalMarkdownRenderer.setEnabled(false) + } + + MarkdownRenderer.JetBrains -> { + kv.set("exp/useKotlinBasedMarkdownRenderer", true) + Experiments.useKotlinBasedMarkdownRenderer.setEnabled(true) + kv.set("exp/useFinalMarkdownRenderer", false) + Experiments.useFinalMarkdownRenderer.setEnabled(false) + } + + MarkdownRenderer.FinalMarkdown -> { + kv.set("exp/useKotlinBasedMarkdownRenderer", false) + Experiments.useKotlinBasedMarkdownRenderer.setEnabled(false) + kv.set("exp/useFinalMarkdownRenderer", true) + Experiments.useFinalMarkdownRenderer.setEnabled(true) + } + } + mdRenderer.value = value } } @@ -98,6 +150,7 @@ class ExperimentsSettingsScreenViewModel : ViewModel() { } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ExperimentsSettingsScreen( navController: NavController, @@ -147,21 +200,63 @@ fun ExperimentsSettingsScreen( Text("Experiments", maxLines = 1, overflow = TextOverflow.Ellipsis) } ) { - ListItem( - headlineContent = { - Text("New Message Markdown Renderer") - }, - supportingContent = { - Text("Use a Kotlin-based Markdown renderer for messages rather than the C++ one. Missing features may be present.") - }, - trailingContent = { - Switch( - checked = viewModel.useKotlinMdRendererChecked.value, - onCheckedChange = null - ) - }, - modifier = Modifier.clickable { viewModel.setUseKotlinMdRendererChecked(!viewModel.useKotlinMdRendererChecked.value) } - ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + ListItem( + headlineContent = { + Text("Markdown Renderer") + }, + supportingContent = { + when (viewModel.mdRenderer.value) { + MarkdownRenderer.Stendal -> Text("Use the original C++ Markdown renderer for messages.") + MarkdownRenderer.JetBrains -> Text("Use the Kotlin-based JetBrains Markdown renderer for messages. This renderer is more feature-complete and has better results.") + MarkdownRenderer.FinalMarkdown -> Text("Use a new. blazingly fast markdown renderer for messages. This renderer is experimental and may have missing features.") + } + }, + modifier = Modifier + .animateContentSize() + .weight(1f) + ) + Column( + modifier = Modifier + .width(IntrinsicSize.Max) + .padding(end = 8.dp), + verticalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), + ) { + ToggleButton( + checked = viewModel.mdRenderer.value == MarkdownRenderer.Stendal, + onCheckedChange = { viewModel.setMdRenderer(MarkdownRenderer.Stendal) }, + modifier = Modifier + .fillMaxWidth() + .semantics { role = Role.RadioButton } + ) { + Text("Default") + } + ToggleButton( + checked = viewModel.mdRenderer.value == MarkdownRenderer.JetBrains, + onCheckedChange = { viewModel.setMdRenderer(MarkdownRenderer.JetBrains) }, + modifier = Modifier + .fillMaxWidth() + .semantics { role = Role.RadioButton } + ) { + Text("Kotlin") + } + if (FeatureFlags.finalMarkdownGranted || viewModel.mdRenderer.value == MarkdownRenderer.FinalMarkdown) { + ToggleButton( + checked = viewModel.mdRenderer.value == MarkdownRenderer.FinalMarkdown, + onCheckedChange = { viewModel.setMdRenderer(MarkdownRenderer.FinalMarkdown) }, + enabled = FeatureFlags.finalMarkdownGranted, + modifier = Modifier + .fillMaxWidth() + .semantics { role = Role.RadioButton } + ) { + Text("Final") + } + } + } + } ListItem( headlineContent = { diff --git a/app/src/main/jniLibs/.gitkeep b/app/src/main/jniLibs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/content/docs/contributing/setup.mdx b/docs/src/content/docs/contributing/setup.mdx index 6bfb440e..741c0737 100644 --- a/docs/src/content/docs/contributing/setup.mdx +++ b/docs/src/content/docs/contributing/setup.mdx @@ -107,6 +107,18 @@ import { Tabs, TabItem, Steps } from "@astrojs/starlight/components" deno run -A scripts/download_deps.ts ``` + :::tip + You can run this script with the `-y` flag to skip confirmation prompts. + ::: + + :::note + This script will query the Revolt version control server for the latest version of the + final-markdown native library and download it. If you do not wish to use pre-built libraries, + you can build final-markdown yourself from the source code at https://git.revolt.chat/android/final-markdown, + and copy the files to the `app/src/main/jniLibs` folder. Note the files that are downloaded are + exactly the same as the ones we use for the app on Google Play, so you can be sure they are safe to use. + ::: + 8. Copy the `revoltbuild.properties.example` file to `revoltbuild.properties` and fill in the required values. ```sh diff --git a/scripts/download_deps.ts b/scripts/download_deps.ts index c9d252ab..cb6078a7 100644 --- a/scripts/download_deps.ts +++ b/scripts/download_deps.ts @@ -1,5 +1,6 @@ import { resolve } from "jsr:@std/path" import { parseArgs } from "jsr:@std/cli/parse-args" +import { ZipReader, Uint8ArrayReader, Uint8ArrayWriter } from "jsr:@zip-js/zip-js" const args = parseArgs(Deno.args, { boolean: ["yes", "y", "auto-accept"], @@ -11,9 +12,11 @@ const args = parseArgs(Deno.args, { const autoAccept = args.yes || args["auto-accept"] const outputFolderParent = resolve(Deno.cwd(), "app", "src", "main", "assets") +const jniLibsFolder = resolve(Deno.cwd(), "app", "src", "main", "jniLibs") try { Deno.statSync(outputFolderParent) + Deno.statSync(jniLibsFolder) } catch (_) { console.error( "\x1b[31m" + // red @@ -45,7 +48,7 @@ try { // Create the output folder Deno.mkdirSync(outputFolder, { recursive: true }) - +/* const deps = [ { file: "katex.min.css", @@ -328,6 +331,82 @@ for (const dep of deps) { const file = resolve(outputFolder, dep.file) Deno.writeFileSync(file, new Uint8Array(data)) console.log(`Downloaded ${dep.file} to ${file}`) +}*/ + +const libsQuery = "https://git.revolt.chat/api/v1/repos/android/final-markdown/releases/latest" + +console.log("The script will now query the Revolt version control server for the latest version" + + " of the final-markdown native library and download it. If you do not wish to use pre-built" + + " libraries, you can build final-markdown yourself from the source code at" + + " https://git.revolt.chat/android/final-markdown, and copy the files to the" + + " app/src/main/jniLibs folder. Note the files that are downloaded are exactly the same as" + + " the ones we use for the app on Google Play, so you can be sure they are safe to use.") + +if (!autoAccept) { + console.log(`Will now query ${libsQuery} for the latest version of the library and download it.`) + if (!confirm("Continue?")) { + console.log("Aborted.") + Deno.exit(0) + } +} else { + console.log(`Will now query ${libsQuery} for the latest version of the library and download it. (auto-accepted)`) } -console.log("Done.") +const queryLibsRes = await fetch(libsQuery) +if (!queryLibsRes.ok) { + console.error(`Failed to fetch the latest library version: ${queryLibsRes.status} ${queryLibsRes.statusText}`) + Deno.exit(1) +} + +const libsJson = await queryLibsRes.json() +const zipUrl = libsJson + .assets + .find((asset: { name: string }) => asset.name === "jniLibs.zip")?.browser_download_url + +if (!zipUrl) { + console.error("No jniLibs.zip found in the latest release.") + Deno.exit(1) +} + +const libsRes = await fetch(zipUrl) +if (!libsRes.ok) { + console.error(`Failed to fetch the jniLibs.zip: ${libsRes.status} ${libsRes.statusText}`) + Deno.exit(1) +} +const libsData = await libsRes.arrayBuffer() + +const libArchitectures = ["arm64-v8a", "armeabi-v7a", "x86", "x86_64"] + +const zipFileBytes = new Uint8Array(libsData) + +const zipReader = new ZipReader(new Uint8ArrayReader(zipFileBytes)) + +const entries = await zipReader.getEntries() + +const createDirPromises = libArchitectures.map(arch => { + const dirPath = resolve(jniLibsFolder, arch) + return Deno.mkdir(dirPath, { recursive: true }) +}) +await Promise.all(createDirPromises) + +const writeFilePromises = libArchitectures.map(async arch => { + const filePathInZip = `${arch}/libfinalmarkdown.so` + + const entry = entries.find(e => e.filename === filePathInZip) + + if (!entry) { + throw new Error(`Expected file not found in zip: ${filePathInZip}`) + } + + const writer = new Uint8ArrayWriter() + const fileData = await entry.getData!(writer) + + const destinationPath = resolve(jniLibsFolder, filePathInZip) + return Deno.writeFile(destinationPath, fileData) +}) + +await Promise.all(writeFilePromises) + +// Close the zip reader +await zipReader.close() + From 7009c9083251a6853f349a61f780c078ac873b65 Mon Sep 17 00:00:00 2001 From: Infi Date: Fri, 22 Aug 2025 01:15:01 +0200 Subject: [PATCH 13/13] fix: uncomment the bulk of download_deps.ts Signed-off-by: Infi --- scripts/download_deps.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/download_deps.ts b/scripts/download_deps.ts index cb6078a7..618b8233 100644 --- a/scripts/download_deps.ts +++ b/scripts/download_deps.ts @@ -48,7 +48,7 @@ try { // Create the output folder Deno.mkdirSync(outputFolder, { recursive: true }) -/* + const deps = [ { file: "katex.min.css", @@ -331,7 +331,7 @@ for (const dep of deps) { const file = resolve(outputFolder, dep.file) Deno.writeFileSync(file, new Uint8Array(data)) console.log(`Downloaded ${dep.file} to ${file}`) -}*/ +} const libsQuery = "https://git.revolt.chat/api/v1/repos/android/final-markdown/releases/latest"