Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
50212c1
feat: migrate from XML Views to Jetpack Compose + Navigation Compose
vitalyster Jul 10, 2026
af53d81
fix: address CodeRabbit review findings
vitalyster Jul 10, 2026
32e74e3
fix: ReplyCard missing like callback, entity overlap check, Cancellat…
vitalyster Jul 10, 2026
28d8eb6
fix: add logging for saveBitmapToFile failures in CropSheet
vitalyster Jul 10, 2026
b5b9a16
fix: track CustomTabs bound state explicitly instead of relying on br…
vitalyster Jul 22, 2026
3eda21e
refactor: remove ViewModels, lift NavHost to top level, fix thread to…
vitalyster Jul 22, 2026
1d8d905
refactor: kill inner NavHost, move all routes to top level
vitalyster Jul 22, 2026
7b9743c
refactor: type-safe navigation routes, per-screen AppScaffold
vitalyster Jul 22, 2026
9fb4ebb
fix: replace rememberCoroutineScope with LaunchedEffect in FeedScreen
vitalyster Jul 22, 2026
bed7e8d
fix: rethrow CancellationException, runCatching swallows it
vitalyster Jul 22, 2026
cf23e1f
fix: merge search input and results into single composable
vitalyster Jul 22, 2026
f7d4ae9
fix: launchSingleTop for search/profile nav, merge NewPost routes
vitalyster Jul 22, 2026
290cfc0
fix: Route.NewPost usage after data object to data class change
vitalyster Jul 22, 2026
e5f0c37
fix: tag insertion, MainActivity notification/updater/event handling
vitalyster Jul 22, 2026
eb7921f
fix: pagination, public route, tests, MainActivity features
vitalyster Jul 22, 2026
ff2bc9a
test: update MainScreenTest for unauthenticated Public route
vitalyster Jul 22, 2026
4ad3f7f
test: fix MainScreenTest for Public route, add notification grant, fi…
vitalyster Jul 22, 2026
ac92532
test: public feed + authenticated bottom tab tests with assumeTrue
vitalyster Jul 22, 2026
ad6a7f9
test: full coverage - public feed (2 tests) + authenticated (2 tests)
vitalyster Jul 22, 2026
e61641c
fix: ChatScreen real-time message receiving via App.instance.messages…
vitalyster Jul 22, 2026
2f765a6
fix: ThreadScreen reply format #mid, auth check, markRead
vitalyster Jul 22, 2026
bcabc57
fix: ChatsListScreen pull-to-refresh
vitalyster Jul 22, 2026
f830043
fix: share intent EXTRA_STREAM support, NewPost uri param
vitalyster Jul 22, 2026
ac1228b
fix: keyboard hide after send in ChatScreen
vitalyster Jul 22, 2026
bb75dd5
feat: NewPostScreen image attachment (gallery/camera/crop/preview)
vitalyster Jul 22, 2026
1d47414
feat: ThreadScreen reply image attachment (gallery picker)
vitalyster Jul 22, 2026
3877397
feat: FeedScreen new posts indicator via App.instance.messages flow
vitalyster Jul 22, 2026
0352289
fix: FeedScreen rememberSaveable for state preservation
vitalyster Jul 22, 2026
37c99b7
feat: PostCard DropdownMenu for Share
vitalyster Jul 22, 2026
17007b6
fix: thread dialog fullscreen, reply-to indicator, hide counters on r…
vitalyster Jul 22, 2026
7c2dddc
fix: icon Send buttons in ChatScreen and NewPostScreen
vitalyster Jul 22, 2026
6a7d619
feat: PostCard context menu matching master - Share, Delete, Privacy …
vitalyster Jul 22, 2026
2fb3c7a
fix: PostCard menu uid comparison for delete/privacy
vitalyster Jul 22, 2026
01145de
fix: pass currentUid to ThreadScreen PostCard for delete menu on own …
vitalyster Jul 22, 2026
ba78227
fix: ChatBubble own messages use primary color
vitalyster Jul 22, 2026
c7e661d
fix: chat links via Android Linkify, underlined, correct colors per b…
vitalyster Jul 22, 2026
e2cd497
feat: ChatScreen TopAppBar with partner uname and back button
vitalyster Jul 22, 2026
bf14a0b
fix: address CR review - deep links, crop sheet, refresh race, back s…
vitalyster Jul 22, 2026
ca43fb1
fix: address remaining CR findings - intent guard, markRead log, MIME…
vitalyster Jul 22, 2026
f0c85f3
refactor: remove unused JuickMessageMenuListener and OnItemClickListener
vitalyster Sep 27, 2026
87a4214
fix: MainActivity intent handling, link routing and notifications
vitalyster Sep 27, 2026
a6ad79b
fix: navigation back stack reset on recreation, duplicate Search route
vitalyster Sep 27, 2026
44195bc
fix: NewPost keeps typed text on tag insert, accepts shared attachment
vitalyster Sep 27, 2026
b7c739c
fix: thread reply target, duplicate replies crash, send feedback
vitalyster Sep 27, 2026
a851554
fix: chat real-time message filtering, ordering and own messages
vitalyster Sep 27, 2026
f3774df
fix: feed pagination errors, duplicate posts across pages, stuck refresh
vitalyster Sep 27, 2026
a33cfaf
fix: PostCard confirms delete, handles privacy toggle failures
vitalyster Sep 27, 2026
64e5c4d
docs: update CLAUDE.md for Compose architecture
vitalyster Sep 27, 2026
42aa108
fix: hide NSFW images when HIDE_NSFW is set, open photo on tap
vitalyster Sep 27, 2026
99c6c1d
feat: show link previews (YouTube) in PostCard
vitalyster Sep 27, 2026
a413cc8
fix: NewPostScreen respects system bar and IME insets
vitalyster Sep 27, 2026
9c99835
fix: thread/chat input bars - reply-to overlap and navigation bar inset
vitalyster Sep 27, 2026
04086fc
fix: chats list shows the last message preview
vitalyster Sep 27, 2026
34e23b7
fix: use string resource for Discussions title and badge description
vitalyster Sep 27, 2026
bf65b8b
fix: NavHost destinations observe current profile instead of a captur…
vitalyster Sep 27, 2026
658e7fd
fix: update thread and feeds after deleting a post or reply
vitalyster Sep 27, 2026
74c7b6e
test: don't remove the device's real account in instrumentation tests
vitalyster Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ jobs:
with:
packages: 'platform-tools'
- name: Build
run: ./gradlew assembleNext
run: ./gradlew assembleDebug
19 changes: 9 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <token>`), Retrofit singleton. `App.instance` accessed everywhere.
- `App.kt` — Application subclass. OkHttp client with auth interceptor (`Authorization: Juick <token>`), 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<Uri>` 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/<flavor>/java/` and `src/<buildType>/`. 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`.
64 changes: 34 additions & 30 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"
Expand All @@ -42,7 +50,6 @@ android {
}
buildFeatures {
buildConfig true
viewBinding true
compose true
}
compileOptions {
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -149,36 +147,42 @@ 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
androidTestImplementation libs.junit
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
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -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
Expand Down
26 changes: 12 additions & 14 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,26 @@ 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"
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"
Expand All @@ -40,43 +38,43 @@ 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" }
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]
Expand Down
2 changes: 1 addition & 1 deletion src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2008-2022, Juick
~ Copyright (C) 2008-2026, Juick
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2008-2026, Juick
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.juick.android.testing

import android.accounts.Account
import android.accounts.AccountManager
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.test.platform.app.InstrumentationRegistry
import com.juick.R
import com.juick.android.MainActivity
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test

class AuthenticatedMainScreenTest {

companion object {
private var testAccount: Account? = null

@JvmStatic
@BeforeClass
fun setupAccount() {
val ctx = InstrumentationRegistry.getInstrumentation().targetContext
val am = AccountManager.get(ctx)
if (am.getAccountsByType(ctx.getString(R.string.applicationId)).isEmpty()) {
testAccount = Account("test", ctx.getString(R.string.applicationId))
am.addAccountExplicitly(testAccount, "test_hash", null)
}
InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand(
"pm grant ${ctx.packageName} android.permission.POST_NOTIFICATIONS"
)
}

@JvmStatic
@AfterClass
fun cleanup() {
val ctx = InstrumentationRegistry.getInstrumentation().targetContext
testAccount?.let { AccountManager.get(ctx).removeAccountExplicitly(it) }
testAccount = null
}
}

@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()

@Test
fun showsBottomNavigation_withThreeTabs() {
val s = composeTestRule.activity
composeTestRule.onNodeWithText(s.getString(R.string.Subscriptions)).assertIsDisplayed()
composeTestRule.onNodeWithText(s.getString(R.string.Discover)).assertIsDisplayed()
composeTestRule.onNodeWithText(s.getString(R.string.PMs)).assertIsDisplayed()
}

@Test
fun showsSearchButton_inTopAppBar() {
composeTestRule.onNodeWithContentDescription(
composeTestRule.activity.getString(R.string.search)
).assertIsDisplayed()
}
}
Loading