diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 4e3eb9aa..4686798b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -24,4 +24,4 @@ jobs: with: packages: 'platform-tools' - name: Build - run: ./gradlew assembleNext + run: ./gradlew assembleDebug diff --git a/CLAUDE.md b/CLAUDE.md index 0cb156e0..e199d453 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,27 +15,26 @@ Build scripts are Groovy DSL (`build.gradle`), not Kotlin DSL. ## Architecture -Juick microblogging Android client. Single-activity (MainActivity) + Navigation Component. minSdk 24, targetSdk 37, Java 17, Kotlin 2.4.10. +Juick microblogging Android client. Single activity (MainActivity) with Jetpack Compose UI and Navigation Compose. minSdk 24, targetSdk 37, Java 17, Kotlin 2.4.20. **Flavors** (`notifications` dimension): `google` (FCM), `huawei` (HMS Push, hides NSFW), `free` (SSE polling). -**Build types**: `debug`, `release`, `next` (Compose UI enabled), `store` (updater disabled). +**Build types**: `debug`, `release`, `store` (updater disabled). **Key files**: -- `App.kt` — Application subclass. OkHttp client with auth interceptor (`Authorization: Juick `), Retrofit singleton. `App.instance` accessed everywhere. +- `App.kt` — Application subclass. OkHttp client with auth interceptor (`Authorization: Juick `), Retrofit singleton, Coil image loader. `App.instance` accessed everywhere. `App.instance.messages` is a StateFlow of posts received via push/SSE; screens observe it for real-time updates. - `api/Api.kt` — Retrofit interface for all Juick API calls. Models in `api/model/` use kotlinx.serialization. -- `android/MainActivity.kt` — Single activity, bottom nav, handles deep links (juick.com URLs), new event intents, ACTION_SEND shares. +- `android/MainActivity.kt` — Single `ComponentActivity`, hosts `AppNavigation`, handles deep links (juick.com URLs), new event intents, ACTION_SEND shares, Custom Tabs. - `android/service/AuthenticationService.kt` — Android AccountManager authenticator (runs in `:auth` process). Auth token = account "hash" stored via AccountManager. - `android/Account.kt` — ViewModel for current user profile. `App.isAuthenticated` / `App.accountData` are extension properties from `AccountHelpers.kt`. -- `android/screens/FeedFragment.kt` + `FeedViewModel.kt` — Reusable paginated feed. Other screens (Home, Discover, Blog, Search, Discussions) extend FeedFragment. ViewModel uses `apiUrl: StateFlow` to reactively fetch. +- `android/ui/navigation/Routes.kt` + `AppNavigation.kt` — Type-safe `@Serializable` routes and the single top-level NavHost. Start: `Route.Home` (authenticated) or `Route.Public`. `Thread` and `Tags` are dialog destinations. +- `android/ui/AppScaffold.kt` — Per-screen scaffold: top bar (search, discussions badge, avatar), bottom nav, FAB. +- `android/ui/screens/feed/FeedScreen.kt` — Reusable paginated feed (Home, Discover, Blog, Search, Discussions). `PostCard.kt` renders a post; `MessageFormatter.kt` turns post entities into annotated text blocks. +- `android/ui/screens/` — `thread`, `chat`, `chats`, `post` (new post), `tags`, `search`, `noauth`. Screens load data directly via `App.instance.api` in `LaunchedEffect`, no ViewModels. - `android/NotificationSender.kt` — Local notifications from push data. Silenced when app in foreground. - `android/updater/Updater.kt` — Checks GitHub Releases for APK updates, matches by flavor name. - `android/LinkPreviewer.kt` — Plugin interface. Google flavor registers `YouTubePreviewer`. -**Auth flow**: `SignInActivity` (nick+pass or Google via `SignInProvider` interface) → `GoogleSignInProvider` in google flavor uses Credential Manager API → `SignUpActivity` for new Google accounts. - -**Compose**: Only in `next` build type. `NextSignInActivity` in `src/next/`. Dependencies: Compose BOM, Material 3. +**Auth flow**: `SignInActivity` (Compose `SignInScreen`; nick+pass or Google via `SignInProvider` interface) → `GoogleSignInProvider` in google flavor uses Credential Manager API → `SignUpActivity` for new Google accounts. **Flavor-specific code** lives in `src//java/` and `src//`. Each flavor has its own `NotificationManager` and `JuickConfig`. - -**Navigation**: `src/main/res/navigation/navigation.xml`. Start: `home`. Key destinations: `discover`, `chats`, `PMFragment` (chat), `thread` (dialog), `blog`, `search`, `tags` (dialog), `discussions`, `new_post`. diff --git a/build.gradle b/build.gradle index 097a6cf0..872430c6 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,15 @@ if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } +configurations.all { + resolutionStrategy.eachDependency { + if (requested.group == "androidx.concurrent" && requested.name == "concurrent-futures") { + useVersion("1.2.0") + because("conflict between compose BOM (1.1.0) and test deps (1.2.0)") + } + } +} + android { compileSdk = 37 defaultConfig { @@ -33,7 +42,6 @@ android { addConstant("INTENT_NEW_EVENT_ACTION", "com.juick.NEW_EVENT_ACTION") buildConfigField "boolean", "HIDE_NSFW", "false" - buildConfigField "boolean", "ENABLE_COMPOSE_UI", "false" buildConfigField "boolean", "ENABLE_UPDATER", "true" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -42,7 +50,6 @@ android { } buildFeatures { buildConfig true - viewBinding true compose true } compileOptions { @@ -66,10 +73,6 @@ android { minifyEnabled false signingConfig signingConfigs.release } - next { - initWith debug - buildConfigField "boolean", "ENABLE_COMPOSE_UI", "true" - } store { initWith release buildConfigField "boolean", "ENABLE_UPDATER", "false" @@ -123,16 +126,11 @@ dependencies { implementation libs.core implementation libs.appcompat implementation libs.browser - implementation libs.navigation.fragment.ktx - implementation libs.navigation.ui.ktx - implementation libs.savedstate.ktx - implementation libs.swiperefreshlayout - implementation libs.lifecycle.viewmodel.compose + implementation libs.lifecycle.runtime.compose + implementation libs.runtime.livedata implementation libs.lifecycle.process implementation libs.lifecycle.service - implementation libs.material - // Sign in with Google googleImplementation libs.credentials.play.services.auth googleImplementation libs.play.services.auth @@ -149,25 +147,30 @@ dependencies { implementation libs.retrofit implementation libs.converter.kotlinx.serialization - implementation libs.chatkit huaweiImplementation libs.push - implementation libs.fragmentviewbindingdelegate.kt - implementation libs.android.image.cropper // Compose - nextImplementation libs.activity.compose - nextImplementation libs.constraintlayout.compose - nextImplementation platform(libs.compose.bom) - nextImplementation libs.ui - nextImplementation libs.ui.graphics - nextImplementation libs.ui.viewbinding - nextImplementation libs.ui.tooling.preview - nextImplementation libs.material.icons.extended - nextImplementation libs.material3 - nextImplementation libs.ui.tooling + implementation libs.activity.compose + implementation libs.constraintlayout.compose + implementation platform(libs.compose.bom) + implementation libs.ui + implementation libs.ui.graphics + implementation libs.ui.tooling.preview + implementation libs.material.icons.extended + implementation libs.material3 + debugImplementation libs.ui.tooling + + // Navigation Compose + implementation libs.navigation.compose + + // Coil for image loading + implementation libs.coil.compose + implementation libs.coil.network.okhttp // Core library + testImplementation libs.junit + androidTestImplementation libs.test.core.ktx // Assertions @@ -175,10 +178,11 @@ dependencies { androidTestImplementation libs.truth androidTestImplementation libs.espresso.core - // Espresso intents for intercepting ACTION_VIEW - androidTestImplementation libs.espresso.intents - // Fragment testing helpers (launchFragmentInContainer) - androidTestImplementation libs.fragment.testing + + // Compose UI testing + androidTestImplementation(platform(libs.compose.bom)) + androidTestImplementation libs.compose.ui.test.junit4 + debugImplementation libs.compose.ui.test.manifest // UI Automator androidTestImplementation libs.uiautomator.v18 } diff --git a/gradle.properties b/gradle.properties index 80cc0346..94fc9956 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ org.gradle.daemon=false -org.gradle.parallel=fasle +org.gradle.parallel=false org.gradle.jvmargs=-Xmx1536M org.gradle.vfs.watch=true android.useAndroidX=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d1e89a7..64982e33 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,20 +4,19 @@ androidImageCropper = "4.7.0" appcompat = "1.8.0" browser = "1.10.0" chatkit = "5e99a8bf3c" +coil = "3.4.0" constraintlayoutCompose = "1.1.2" credentialsPlayServicesAuth = "1.6.0" -espressoCore = "3.6.1" -espressoIntents = "3.7.0" +espresso = "3.7.0" firebaseBom = "34.19.0" composeBom = "2026.09.00" fragmentTesting = "1.9.0" -fragmentviewbindingdelegateKt = "1.0.2" googleid = "1.2.1" junit = "1.3.0" kotlinxSerializationJson = "1.11.0" core = "1.19.0" lifecycleVersion = "2.11.0" -material = "1.14.0" +material3 = "1.4.0" navigationFragmentKtx = "2.10.1" okhttp = "5.5.0" oksse = "0.9.0" @@ -25,7 +24,6 @@ playServicesAuth = "22.0.0" push = "6.13.0.301" retrofit = "3.0.0" savedstateKtx = "1.5.0" -swiperefreshlayout = "1.2.0" truth = "1.7.0" uiautomatorV18 = "2.1.3" testVersion = "1.7.0" @@ -40,35 +38,36 @@ android-image-cropper = { module = "com.vanniktech:android-image-cropper", versi browser = { module = "androidx.browser:browser", version.ref = "browser" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } chatkit = { module = "com.github.vitalyster:ChatKit", version.ref = "chatkit" } +coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } +coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" } converter-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } core = { module = "androidx.core:core", version.ref = "core" } credentials-play-services-auth = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "credentialsPlayServicesAuth" } -espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } -espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espressoIntents" } +espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } firebase-messaging = { module = "com.google.firebase:firebase-messaging" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } fragment-testing = { module = "androidx.fragment:fragment-testing", version.ref = "fragmentTesting" } -fragmentviewbindingdelegate-kt = { module = "com.github.Zhuinden:fragmentviewbindingdelegate-kt", version.ref = "fragmentviewbindingdelegateKt" } googleid = { module = "com.google.android.libraries.identity.googleid:googleid", version.ref = "googleid" } junit = { module = "androidx.test.ext:junit", version.ref = "junit" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleVersion" } +lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleVersion" } +runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" } +compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycleVersion" } lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycleVersion" } logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } -material = { module = "com.google.android.material:material", version.ref = "material" } -material3 = { module = "androidx.compose.material3:material3" } +material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } -navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } -navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigationFragmentKtx" } +navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationFragmentKtx" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } oksse = { module = "com.github.heremaps:oksse", version.ref = "oksse" } play-services-auth = { module = "com.google.android.gms:play-services-auth", version.ref = "playServicesAuth" } push = { module = "com.huawei.hms:push", version.ref = "push" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } -swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" } savedstate-ktx = { module = "androidx.savedstate:savedstate-ktx", version.ref = "savedstateKtx" } truth = { module = "androidx.test.ext:truth", version.ref = "truth" } test-core-ktx = { module = "androidx.test:core-ktx", version.ref = "testVersion" } @@ -76,7 +75,6 @@ ui-graphics = { module = "androidx.compose.ui:ui-graphics" } ui = { module = "androidx.compose.ui:ui" } ui-tooling = { module = "androidx.compose.ui:ui-tooling" } ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } -ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding" } uiautomator-v18 = { module = "com.android.support.test.uiautomator:uiautomator-v18", version.ref = "uiautomatorV18" } [plugins] diff --git a/src/androidTest/AndroidManifest.xml b/src/androidTest/AndroidManifest.xml index ae8f37a8..d2084741 100644 --- a/src/androidTest/AndroidManifest.xml +++ b/src/androidTest/AndroidManifest.xml @@ -1,6 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/res/layout/activity_main.xml b/src/main/res/layout/activity_main.xml deleted file mode 100644 index 0baf0272..00000000 --- a/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/res/layout/activity_signup.xml b/src/main/res/layout/activity_signup.xml deleted file mode 100644 index 3f73ddc0..00000000 --- a/src/main/res/layout/activity_signup.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/main/res/layout/content_main.xml b/src/main/res/layout/content_main.xml deleted file mode 100644 index f31aa9ec..00000000 --- a/src/main/res/layout/content_main.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/main/res/layout/dialog_crop.xml b/src/main/res/layout/dialog_crop.xml deleted file mode 100644 index 1ab2b671..00000000 --- a/src/main/res/layout/dialog_crop.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - -