From ff72462be5fc931c7f0c0d20aedcbb335425d2f7 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 10 Sep 2022 15:35:48 +0100 Subject: [PATCH 01/16] fixing firebase classes being imported for foss variant - the notification module was pulling in firebase for an annotation import, which in turn pulled in the .aar manifest entries and services etc, fixed by removing the unneeded dependency --- features/notifications/build.gradle | 2 -- .../app/dapk/st/notifications/AndroidNotificationStyle.kt | 3 --- .../dapk/st/notifications/AndroidNotificationStyleBuilder.kt | 3 --- 3 files changed, 8 deletions(-) diff --git a/features/notifications/build.gradle b/features/notifications/build.gradle index 377a496..85ca262 100644 --- a/features/notifications/build.gradle +++ b/features/notifications/build.gradle @@ -12,8 +12,6 @@ dependencies { implementation project(":features:messenger") implementation project(":features:navigator") - implementation platform('com.google.firebase:firebase-bom:29.0.3') - implementation 'com.google.firebase:firebase-messaging' implementation Dependencies.mavenCentral.kotlinSerializationJson kotlinTest(it) diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationStyle.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationStyle.kt index 05dd95e..d163192 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationStyle.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationStyle.kt @@ -2,8 +2,6 @@ package app.dapk.st.notifications import android.app.Notification import android.graphics.drawable.Icon -import android.os.Build -import androidx.annotation.RequiresApi sealed interface AndroidNotificationStyle { @@ -20,7 +18,6 @@ sealed interface AndroidNotificationStyle { val content: List, ) : AndroidNotificationStyle { - @RequiresApi(Build.VERSION_CODES.P) override fun build(builder: AndroidNotificationStyleBuilder) = builder.build(this) data class AndroidPerson(val name: String, val key: String, val icon: Icon? = null) diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationStyleBuilder.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationStyleBuilder.kt index ec91fba..898d0a9 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationStyleBuilder.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationStyleBuilder.kt @@ -5,8 +5,6 @@ import android.app.Notification import android.app.Notification.InboxStyle import android.app.Notification.MessagingStyle import android.app.Person -import android.os.Build -import androidx.annotation.RequiresApi @SuppressLint("NewApi") class AndroidNotificationStyleBuilder( @@ -27,7 +25,6 @@ class AndroidNotificationStyleBuilder( inboxStyle.setSummaryText(summary) } - @RequiresApi(Build.VERSION_CODES.P) private fun AndroidNotificationStyle.Messaging.buildMessagingStyle() = messagingStyleFactory( personBuilderFactory() .setName(person.name) From 7554eac7afcf5d9492e731aeeebe1b2d80e8f6dc Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 10 Sep 2022 17:10:48 +0100 Subject: [PATCH 02/16] adding build script for generating signed foss releases --- tools/generate-fdroid-release.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100755 tools/generate-fdroid-release.sh diff --git a/tools/generate-fdroid-release.sh b/tools/generate-fdroid-release.sh new file mode 100755 index 0000000..bf4d074 --- /dev/null +++ b/tools/generate-fdroid-release.sh @@ -0,0 +1,21 @@ +#! /bin/bash +set -e + +WORKING_DIR=app/build/outputs/apk/release +UNSIGNED=$WORKING_DIR/app-foss-release-unsigned.apk +ALIGNED_UNSIGNED=$WORKING_DIR/app-foss-release-unsigned-aligned.apk +SIGNED=$WORKING_DIR/app-foss-release-signed.apk + +ZIPALIGN=$(find "$ANDROID_HOME" -iname zipalign -print -quit) +APKSIGNER=$(find "$ANDROID_HOME" -iname apksigner -print -quit) + +./gradlew clean assembleRelease -Pfoss -Punsigned --no-daemon --no-configuration-cache --no-build-cache + +$ZIPALIGN -v -p 4 $UNSIGNED $ALIGNED_UNSIGNED + +$APKSIGNER sign \ + --ks .secrets/fdroid.keystore \ + --ks-key-alias key0 \ + --ks-pass pass:$1 \ + --out $SIGNED \ + $ALIGNED_UNSIGNED From 28fa985923cc3f5c4c12e01eec17b43d76f27169 Mon Sep 17 00:00:00 2001 From: Izzy Date: Sat, 10 Sep 2022 18:42:57 +0200 Subject: [PATCH 03/16] initial fastlane structures --- .../metadata/android/en-US/full_description.txt | 11 +++++++++++ .../android/en-US/images/featureGraphic.jpg | Bin 0 -> 30190 bytes fastlane/metadata/android/en-US/images/icon.png | Bin 0 -> 4127 bytes .../android/en-US/short_description.txt | 1 + 4 files changed, 12 insertions(+) create mode 100644 fastlane/metadata/android/en-US/full_description.txt create mode 100644 fastlane/metadata/android/en-US/images/featureGraphic.jpg create mode 100644 fastlane/metadata/android/en-US/images/icon.png create mode 100644 fastlane/metadata/android/en-US/short_description.txt diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 0000000..0609e33 --- /dev/null +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,11 @@ +SmallTalk is a minimal messenger using the Matrix protocol. It has a comparingly tiny app size and focuses on reliability and stability. Its feature set is rather bare-bones: + +* Login with Matrix ID/Password +* Combined Room and DM interface +* End to end encryption +* Message bubbles, supporting text, replies and edits +* Push notifications (DMs always notify, Rooms notify once) +* Importing of E2E room keys from Element clients +* UnifiedPush + +More to come in the future. diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.jpg b/fastlane/metadata/android/en-US/images/featureGraphic.jpg new file mode 100644 index 0000000000000000000000000000000000000000..608f98125fcc6b88e881499edf778c53cc991978 GIT binary patch literal 30190 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 NsaCeaK>p|B{{d9vd5!=8 literal 0 HcmV?d00001 diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..34ebadfc3a4ed4a6158160255a7a446081b12e0e GIT binary patch literal 4127 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 XmAI(6TcgQ7@pD)q?GRO_zRCXwM(Up@ literal 0 HcmV?d00001 diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 0000000..f052111 --- /dev/null +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +Minimal Android messenger powered by Matrix \ No newline at end of file From a109ec6cff570f0dc7b7be81444373b002bcedf3 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 10 Sep 2022 19:29:45 +0100 Subject: [PATCH 04/16] observe invite changes and render a ! badge on the profile bottom item --- .../kotlin/app/dapk/st/graph/AppModule.kt | 2 +- features/home/build.gradle | 1 + .../kotlin/app/dapk/st/home/HomeModule.kt | 3 + .../kotlin/app/dapk/st/home/HomeScreen.kt | 66 ++++++++++--------- .../main/kotlin/app/dapk/st/home/HomeState.kt | 2 +- .../kotlin/app/dapk/st/home/HomeViewModel.kt | 48 ++++++++++++-- .../kotlin/app/dapk/st/home/MainActivity.kt | 8 ++- 7 files changed, 92 insertions(+), 38 deletions(-) diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index ff26c53..d8874d9 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -181,7 +181,7 @@ internal class FeatureModules internal constructor( clock ) } - val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile, buildMeta) } + val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile, matrixModules.sync, buildMeta) } val settingsModule by unsafeLazy { SettingsModule( storeModule.value, diff --git a/features/home/build.gradle b/features/home/build.gradle index 5507d8c..3265a1f 100644 --- a/features/home/build.gradle +++ b/features/home/build.gradle @@ -3,6 +3,7 @@ applyAndroidComposeLibraryModule(project) dependencies { implementation project(":matrix:services:profile") implementation project(":matrix:services:crypto") + implementation project(":matrix:services:sync") implementation project(":features:directory") implementation project(":features:login") implementation project(":features:settings") diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt index 1d33dd8..b49c5cc 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt @@ -6,11 +6,13 @@ import app.dapk.st.directory.DirectoryViewModel import app.dapk.st.domain.StoreModule import app.dapk.st.login.LoginViewModel import app.dapk.st.matrix.room.ProfileService +import app.dapk.st.matrix.sync.SyncService import app.dapk.st.profile.ProfileViewModel class HomeModule( private val storeModule: StoreModule, private val profileService: ProfileService, + private val syncService: SyncService, private val buildMeta: BuildMeta, ) : ProvidableModule { @@ -26,6 +28,7 @@ class HomeModule( storeModule.applicationStore(), buildMeta, ), + syncService, ) } diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt index 57f852a..4590076 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt @@ -3,13 +3,12 @@ package app.dapk.st.home import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.components.CenteredLoading import app.dapk.st.design.components.CircleishAvatar -import app.dapk.st.design.components.SmallTalkTheme import app.dapk.st.directory.DirectoryScreen import app.dapk.st.home.HomeScreenState.* import app.dapk.st.home.HomeScreenState.Page.Directory @@ -20,41 +19,42 @@ import app.dapk.st.profile.ProfileScreen @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen(homeViewModel: HomeViewModel) { - Surface(Modifier.fillMaxSize()) { - LaunchedEffect(true) { - homeViewModel.start() - } + LifecycleEffect( + onStart = { homeViewModel.start() }, + onStop = { homeViewModel.stop() } + ) - when (val state = homeViewModel.state) { - Loading -> CenteredLoading() - is SignedIn -> { - Scaffold( - bottomBar = { - BottomBar(state, homeViewModel) - }, - content = { innerPadding -> - Box(modifier = Modifier.padding(innerPadding)) { - when (state.page) { - Directory -> DirectoryScreen(homeViewModel.directory()) - Profile -> { - ProfileScreen(homeViewModel.profile()) { - homeViewModel.changePage(Directory) - } - } + when (val state = homeViewModel.state) { + Loading -> CenteredLoading() + is SignedIn -> { + Scaffold( + bottomBar = { + BottomBar(state, homeViewModel) + }, + content = { innerPadding -> + Box(modifier = Modifier.padding(innerPadding)) { + when (state.page) { + Directory -> DirectoryScreen(homeViewModel.directory()) + Profile -> { + ProfileScreen(homeViewModel.profile()) { + homeViewModel.changePage(Directory) } } } - ) - } - SignedOut -> { - LoginScreen(homeViewModel.login()) { - homeViewModel.loggedIn() } } + ) + } + + SignedOut -> { + LoginScreen(homeViewModel.login()) { + homeViewModel.loggedIn() } } + } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun BottomBar(state: SignedIn, homeViewModel: HomeViewModel) { Column { @@ -72,11 +72,17 @@ private fun BottomBar(state: SignedIn, homeViewModel: HomeViewModel) { } }, ) - Profile -> NavigationBarItem( + Profile -> NavigationBarItem( icon = { - Box { - CircleishAvatar(state.me.avatarUrl?.value, state.me.displayName ?: state.me.userId.value, size = 25.dp) + BadgedBox(badge = { + if (state.invites > 0) { + Badge(containerColor = MaterialTheme.colorScheme.primary) { Text("!", color = MaterialTheme.colorScheme.onPrimary) } + } + }) { + Box { + CircleishAvatar(state.me.avatarUrl?.value, state.me.displayName ?: state.me.userId.value, size = 25.dp) + } } }, selected = state.page == page, diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt index ca25e6f..7bf0114 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt @@ -10,7 +10,7 @@ sealed interface HomeScreenState { object Loading : HomeScreenState object SignedOut : HomeScreenState - data class SignedIn(val page: Page, val me: ProfileService.Me) : HomeScreenState + data class SignedIn(val page: Page, val me: ProfileService.Me, val invites: Int) : HomeScreenState enum class Page(val icon: ImageVector) { Directory(Icons.Filled.Menu), diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt index 877314a..334660b 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt @@ -8,8 +8,15 @@ import app.dapk.st.login.LoginViewModel import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.isSignedIn import app.dapk.st.matrix.room.ProfileService +import app.dapk.st.matrix.sync.SyncService import app.dapk.st.profile.ProfileViewModel import app.dapk.st.viewmodel.DapkViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch class HomeViewModel( @@ -20,10 +27,13 @@ class HomeViewModel( private val profileService: ProfileService, private val cacheCleaner: StoreCleaner, private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase, + private val syncService: SyncService, ) : DapkViewModel( initialState = Loading ) { + private var listenForInvitesJob: Job? = null + fun directory() = directoryViewModel fun login() = loginViewModel fun profile() = profileViewModel @@ -31,21 +41,47 @@ class HomeViewModel( fun start() { viewModelScope.launch { state = if (credentialsProvider.isSignedIn()) { - val me = profileService.me(forceRefresh = false) - SignedIn(Page.Directory, me) + initialHomeContent() } else { SignedOut } } + + viewModelScope.launch { + if (credentialsProvider.isSignedIn()) { + listenForInviteChanges() + } + } + + } + + private suspend fun initialHomeContent(): SignedIn { + val me = profileService.me(forceRefresh = false) + val initialInvites = syncService.invites().first().size + return SignedIn(Page.Directory, me, invites = initialInvites) } fun loggedIn() { viewModelScope.launch { - val me = profileService.me(forceRefresh = false) - state = SignedIn(Page.Directory, me) + state = initialHomeContent() + listenForInviteChanges() } } + private fun CoroutineScope.listenForInviteChanges() { + listenForInvitesJob?.cancel() + listenForInvitesJob = syncService.invites() + .onEach { invites -> + when (val currentState = state) { + is SignedIn -> updateState { currentState.copy(invites = invites.size) } + Loading, + SignedOut -> { + // do nothing + } + } + }.launchIn(this) + } + fun hasVersionChanged() = betaVersionUpgradeUseCase.hasVersionChanged() fun clearCache() { @@ -66,4 +102,8 @@ class HomeViewModel( SignedOut -> current } } + + fun stop() { + viewModelScope.cancel() + } } diff --git a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt index cd9872b..609f52d 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt @@ -1,11 +1,13 @@ package app.dapk.st.home import android.os.Bundle -import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import app.dapk.st.core.DapkActivity import app.dapk.st.core.module @@ -35,7 +37,9 @@ class MainActivity : DapkActivity() { if (homeViewModel.hasVersionChanged()) { BetaUpgradeDialog() } else { - HomeScreen(homeViewModel) + Surface(Modifier.fillMaxSize()) { + HomeScreen(homeViewModel) + } } } } From 577e692e32e3c26d80b17d59b923dfcc1ec1c957 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 10 Sep 2022 19:50:19 +0100 Subject: [PATCH 05/16] removing invites from the database after rejecting them and allowing them to fail with 403 (if the user no longer has access) --- .../kotlin/app/dapk/st/graph/AppModule.kt | 6 ++++- .../app/dapk/st/profile/ProfileViewModel.kt | 5 ++++- .../app/dapk/st/matrix/room/RoomService.kt | 10 ++++++++- .../room/internal/DefaultRoomService.kt | 22 ++++++++++++++++++- .../matrix/room/internal/RoomInviteRemover.kt | 7 ++++++ 5 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomInviteRemover.kt diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index d8874d9..e56cf2e 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -303,6 +303,7 @@ internal class MatrixModules( } } + val overviewStore = store.overviewStore() installRoomService( storeModule.value.memberStore(), roomMessenger = { @@ -316,6 +317,9 @@ internal class MatrixModules( ) } } + }, + roomInviteRemover = { + overviewStore.removeInvites(listOf(it)) } ) @@ -323,7 +327,7 @@ internal class MatrixModules( installSyncService( credentialsStore, - store.overviewStore(), + overviewStore, store.roomStore(), store.syncStore(), store.filterStore(), diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt index ff4a017..ab7fb5b 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt @@ -1,5 +1,6 @@ package app.dapk.st.profile +import android.util.Log import androidx.lifecycle.viewModelScope import app.dapk.st.core.Lce import app.dapk.st.core.extensions.ErrorTracker @@ -95,7 +96,9 @@ class ProfileViewModel( fun rejectRoomInvite(roomId: RoomId) { launchCatching { roomService.rejectJoinRoom(roomId) }.fold( - onError = {} + onError = { + Log.e("!!!", it.message, it) + } ) } diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt index 92bc1d0..1f933a9 100644 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt +++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt @@ -9,6 +9,7 @@ import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.room.internal.DefaultRoomService +import app.dapk.st.matrix.room.internal.RoomInviteRemover import app.dapk.st.matrix.room.internal.RoomMembers import app.dapk.st.matrix.room.internal.RoomMembersCache @@ -40,9 +41,16 @@ interface RoomService : MatrixService { fun MatrixServiceInstaller.installRoomService( memberStore: MemberStore, roomMessenger: ServiceDepFactory, + roomInviteRemover: RoomInviteRemover, ) { this.install { (httpClient, _, services, logger) -> - SERVICE_KEY to DefaultRoomService(httpClient, logger, RoomMembers(memberStore, RoomMembersCache()), roomMessenger.create(services)) + SERVICE_KEY to DefaultRoomService( + httpClient, + logger, + RoomMembers(memberStore, RoomMembersCache()), + roomMessenger.create(services), + roomInviteRemover + ) } } diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt index 4ad8ef5..2448e4c 100644 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt +++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt @@ -8,6 +8,8 @@ import app.dapk.st.matrix.http.emptyJsonBody import app.dapk.st.matrix.http.jsonBody import app.dapk.st.matrix.room.RoomMessenger import app.dapk.st.matrix.room.RoomService +import io.ktor.client.plugins.* +import io.ktor.http.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -16,7 +18,9 @@ class DefaultRoomService( private val logger: MatrixLogger, private val roomMembers: RoomMembers, private val roomMessenger: RoomMessenger, + private val roomInviteRemover: RoomInviteRemover, ) : RoomService { + override suspend fun joinedMembers(roomId: RoomId): List { val response = httpClient.execute(joinedMembersRequest(roomId)) return response.joined.map { (userId, member) -> @@ -68,7 +72,23 @@ class DefaultRoomService( } override suspend fun rejectJoinRoom(roomId: RoomId) { - httpClient.execute(rejectJoinRoomRequest(roomId)) + runCatching { httpClient.execute(rejectJoinRoomRequest(roomId)) }.fold( + onSuccess = {}, + onFailure = { + when (it) { + is ClientRequestException -> { + if (it.response.status == HttpStatusCode.Forbidden) { + // allow error + } else { + throw it + } + } + + else -> throw it + } + } + ) + roomInviteRemover.remove(roomId) } } diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomInviteRemover.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomInviteRemover.kt new file mode 100644 index 0000000..e5da0a9 --- /dev/null +++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomInviteRemover.kt @@ -0,0 +1,7 @@ +package app.dapk.st.matrix.room.internal + +import app.dapk.st.matrix.common.RoomId + +fun interface RoomInviteRemover { + suspend fun remove(roomId: RoomId) +} \ No newline at end of file From 514b25caea9c868315cb2617443645824c337705 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 11 Sep 2022 11:36:16 +0100 Subject: [PATCH 06/16] fixing memory _leak_ on android Q when exiting screens --- .../src/main/kotlin/app/dapk/st/core/DapkActivity.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt index 59cec14..29ee41b 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt @@ -1,5 +1,6 @@ package app.dapk.st.core +import android.os.Build import android.os.Bundle import android.view.WindowManager import androidx.activity.ComponentActivity @@ -20,10 +21,13 @@ abstract class DapkActivity : ComponentActivity(), EffectScope { private lateinit var themeConfig: ThemeConfig + private val needsBackLeakWorkaround = Build.VERSION.SDK_INT == Build.VERSION_CODES.Q + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) this.themeConfig = ThemeConfig(themeStore.isMaterialYouEnabled()) + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); } @@ -53,4 +57,11 @@ abstract class DapkActivity : ComponentActivity(), EffectScope { } } } + + override fun onBackPressed() { + if (needsBackLeakWorkaround && !onBackPressedDispatcher.hasEnabledCallbacks()) { + finishAfterTransition() + } else + super.onBackPressed() + } } From defabfbae51636d152358a743adfd1385380bab3 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 11 Sep 2022 11:41:28 +0100 Subject: [PATCH 07/16] rendering invites as notification which temporarily open the app on tap --- .../app/dapk/st/SmallTalkApplication.kt | 2 +- .../kotlin/app/dapk/st/graph/AppModule.kt | 1 + .../AndroidNotificationBuilder.kt | 4 ++ .../st/notifications/NotificationChannels.kt | 13 +++++ .../st/notifications/NotificationFactory.kt | 17 +++++++ .../NotificationInviteRenderer.kt | 25 ++++++++++ ...erer.kt => NotificationMessageRenderer.kt} | 2 +- .../st/notifications/NotificationsModule.kt | 45 ++++++++++------- .../ObserveInviteNotificationsUseCase.kt | 49 +++++++++++++++++++ .../RenderNotificationsUseCase.kt | 18 ++++--- .../notifications/NotificationFactoryTest.kt | 29 +++++++++++ .../notifications/NotificationRendererTest.kt | 2 +- .../RenderNotificationsUseCaseTest.kt | 26 ++++++---- .../fake/FakeNotificationInviteRenderer.kt | 8 +++ ....kt => FakeNotificationMessageRenderer.kt} | 8 +-- .../FakeObserveInviteNotificationsUseCase.kt | 10 ++++ .../src/test/kotlin/test/TestMatrix.kt | 3 +- tools/coverage.gradle | 9 ++++ 18 files changed, 229 insertions(+), 42 deletions(-) create mode 100644 features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt rename features/notifications/src/main/kotlin/app/dapk/st/notifications/{NotificationRenderer.kt => NotificationMessageRenderer.kt} (98%) create mode 100644 features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt create mode 100644 features/notifications/src/test/kotlin/fake/FakeNotificationInviteRenderer.kt rename features/notifications/src/test/kotlin/fake/{FakeNotificationRenderer.kt => FakeNotificationMessageRenderer.kt} (81%) create mode 100644 features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt diff --git a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt index 1d15604..3fb4488 100644 --- a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt +++ b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt @@ -60,7 +60,7 @@ class SmallTalkApplication : Application(), ModuleProvider { applicationScope.launch { val notificationsUseCase = notificationsModule.notificationsUseCase() - notificationsUseCase.listenForNotificationChanges() + notificationsUseCase.listenForNotificationChanges(this) } } diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index e56cf2e..8b013f4 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -199,6 +199,7 @@ internal class FeatureModules internal constructor( NotificationsModule( imageLoaderModule.iconLoader(), storeModule.value.roomStore(), + storeModule.value.overviewStore(), context, intentFactory = coreAndroidModule.intentFactory(), dispatchers = coroutineDispatchers, diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationBuilder.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationBuilder.kt index 6f49daa..5cfbd09 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationBuilder.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationBuilder.kt @@ -32,6 +32,8 @@ class AndroidNotificationBuilder( .apply { setGroupSummary(notification.isGroupSummary) } .ifNotNull(notification.groupId) { setGroup(it) } .ifNotNull(notification.messageStyle) { style = it.build(notificationStyleBuilder) } + .ifNotNull(notification.contentTitle) { setContentTitle(it) } + .ifNotNull(notification.contentText) { setContentText(it) } .ifNotNull(notification.contentIntent) { setContentIntent(it) } .ifNotNull(notification.whenTimestamp) { setShowWhen(true) @@ -65,6 +67,8 @@ data class AndroidNotification( val shortcutId: String? = null, val alertMoreThanOnce: Boolean, val contentIntent: PendingIntent? = null, + val contentTitle: String? = null, + val contentText: String? = null, val messageStyle: AndroidNotificationStyle? = null, val category: String? = null, val smallIcon: Int? = null, diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt index 12dbd8d..0daa08c 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt @@ -8,6 +8,7 @@ import android.os.Build const val DIRECT_CHANNEL_ID = "direct_channel_id" const val GROUP_CHANNEL_ID = "group_channel_id" const val SUMMARY_CHANNEL_ID = "summary_channel_id" +const val INVITE_CHANNEL_ID = "invite_channel_id" private const val CHATS_NOTIFICATION_GROUP_ID = "chats_notification_group" @@ -45,6 +46,18 @@ class NotificationChannels( ) } + if (notificationManager.getNotificationChannel(INVITE_CHANNEL_ID) == null) { + notificationManager.createNotificationChannel( + NotificationChannel( + INVITE_CHANNEL_ID, + "Invite notifications", + NotificationManager.IMPORTANCE_DEFAULT, + ).also { + it.group = CHATS_NOTIFICATION_GROUP_ID + } + ) + } + if (notificationManager.getNotificationChannel(SUMMARY_CHANNEL_ID) == null) { notificationManager.createNotificationChannel( NotificationChannel( diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt index 13d26a1..503e074 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt @@ -8,6 +8,7 @@ import app.dapk.st.imageloader.IconLoader import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.navigator.IntentFactory +import java.time.Clock private const val GROUP_ID = "st" @@ -17,6 +18,7 @@ class NotificationFactory( private val intentFactory: IntentFactory, private val iconLoader: IconLoader, private val deviceMeta: DeviceMeta, + private val clock: Clock, ) { private val shouldAlwaysAlertDms = true @@ -84,6 +86,21 @@ class NotificationFactory( category = Notification.CATEGORY_MESSAGE, ) } + + fun createInvite(inviteNotification: InviteNotification): AndroidNotification { + val openAppIntent = intentFactory.notificationOpenApp(context) + return AndroidNotification( + channelId = INVITE_CHANNEL_ID, + smallIcon = R.drawable.ic_notification_small_icon, + whenTimestamp = clock.millis(), + alertMoreThanOnce = true, + contentTitle = "Invite", + contentText = inviteNotification.content, + contentIntent = openAppIntent, + category = Notification.CATEGORY_EVENT, + autoCancel = true, + ) + } } private fun List.mostRecent() = this.sortedBy { it.notification.whenTimestamp }.first() diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt new file mode 100644 index 0000000..8987ea9 --- /dev/null +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt @@ -0,0 +1,25 @@ +package app.dapk.st.notifications + +import android.app.NotificationManager + +private const val INVITE_NOTIFICATION_ID = 103 + +class NotificationInviteRenderer( + private val notificationManager: NotificationManager, + private val notificationFactory: NotificationFactory, + private val androidNotificationBuilder: AndroidNotificationBuilder, +) { + + fun render(inviteNotification: InviteNotification) { + notificationManager.notify( + inviteNotification.roomId.value, + INVITE_NOTIFICATION_ID, + inviteNotification.toAndroidNotification() + ) + } + + private fun InviteNotification.toAndroidNotification() = androidNotificationBuilder.build( + notificationFactory.createInvite(this) + ) + +} \ No newline at end of file diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt similarity index 98% rename from features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt rename to features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt index 5a2cc7c..1a12ddb 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.withContext private const val SUMMARY_NOTIFICATION_ID = 101 private const val MESSAGE_NOTIFICATION_ID = 100 -class NotificationRenderer( +class NotificationMessageRenderer( private val notificationManager: NotificationManager, private val notificationStateMapper: NotificationStateMapper, private val androidNotificationBuilder: AndroidNotificationBuilder, diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt index f3501b9..3368110 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt @@ -6,37 +6,46 @@ import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.DeviceMeta import app.dapk.st.core.ProvidableModule import app.dapk.st.imageloader.IconLoader +import app.dapk.st.matrix.sync.OverviewStore import app.dapk.st.matrix.sync.RoomStore import app.dapk.st.navigator.IntentFactory +import java.time.Clock class NotificationsModule( private val iconLoader: IconLoader, private val roomStore: RoomStore, + private val overviewStore: OverviewStore, private val context: Context, private val intentFactory: IntentFactory, private val dispatchers: CoroutineDispatchers, private val deviceMeta: DeviceMeta, ) : ProvidableModule { - fun notificationsUseCase() = RenderNotificationsUseCase( - notificationRenderer = NotificationRenderer( - notificationManager(), - NotificationStateMapper( - RoomEventsToNotifiableMapper(), - NotificationFactory( - context, - NotificationStyleFactory(iconLoader, deviceMeta), - intentFactory, - iconLoader, - deviceMeta, - ) - ), - AndroidNotificationBuilder(context, deviceMeta, AndroidNotificationStyleBuilder()), + fun notificationsUseCase(): RenderNotificationsUseCase { + val notificationManager = notificationManager() + val androidNotificationBuilder = AndroidNotificationBuilder(context, deviceMeta, AndroidNotificationStyleBuilder()) + val notificationFactory = NotificationFactory( + context, + NotificationStyleFactory(iconLoader, deviceMeta), + intentFactory, + iconLoader, + deviceMeta, + Clock.systemUTC(), + ) + val notificationMessageRenderer = NotificationMessageRenderer( + notificationManager, + NotificationStateMapper(RoomEventsToNotifiableMapper(), notificationFactory), + androidNotificationBuilder, dispatchers - ), - observeRenderableUnreadEventsUseCase = ObserveUnreadNotificationsUseCaseImpl(roomStore), - notificationChannels = NotificationChannels(notificationManager()), - ) + ) + return RenderNotificationsUseCase( + notificationRenderer = notificationMessageRenderer, + observeRenderableUnreadEventsUseCase = ObserveUnreadNotificationsUseCaseImpl(roomStore), + notificationChannels = NotificationChannels(notificationManager), + observeInviteNotificationsUseCase = ObserveInviteNotificationsUseCaseImpl(overviewStore), + inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder) + ) + } private fun notificationManager() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt new file mode 100644 index 0000000..802fb24 --- /dev/null +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt @@ -0,0 +1,49 @@ +package app.dapk.st.notifications + +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.sync.InviteMeta +import app.dapk.st.matrix.sync.OverviewStore +import app.dapk.st.matrix.sync.RoomInvite +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* + +internal typealias ObserveInviteNotificationsUseCase = suspend () -> Flow + +class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase { + + override suspend fun invoke(): Flow { + return overviewStore.latestInvites() + .diff() + .flatten() + .map { + val text = when (val meta = it.inviteMeta) { + InviteMeta.DirectMessage -> "${it.inviterName()} has invited you to chat" + is InviteMeta.Room -> "${it.inviterName()} has invited you to ${meta.roomName ?: "unnamed room"}" + } + InviteNotification(content = text, roomId = it.roomId) + } + } + + private fun Flow>.diff(): Flow> { + val previousInvites = mutableSetOf() + return this.distinctUntilChanged() + .map { + val diff = it.toSet() - previousInvites + previousInvites.clear() + previousInvites.addAll(it) + diff + } + } + + private fun RoomInvite.inviterName() = this.from.displayName?.let { "$it (${this.from.id.value})" } ?: this.from.id.value +} + +@OptIn(FlowPreview::class) +private fun Flow>.flatten() = this.flatMapConcat { items -> + flow { items.forEach { this.emit(it) } } +} + +data class InviteNotification( + val content: String, + val roomId: RoomId +) \ No newline at end of file diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt index 59128eb..d51f6e8 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt @@ -2,21 +2,27 @@ package app.dapk.st.notifications import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomOverview -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart class RenderNotificationsUseCase( - private val notificationRenderer: NotificationRenderer, + private val notificationRenderer: NotificationMessageRenderer, + private val inviteRenderer: NotificationInviteRenderer, private val observeRenderableUnreadEventsUseCase: ObserveUnreadNotificationsUseCase, + private val observeInviteNotificationsUseCase: ObserveInviteNotificationsUseCase, private val notificationChannels: NotificationChannels, ) { - suspend fun listenForNotificationChanges() { + suspend fun listenForNotificationChanges(scope: CoroutineScope) { + notificationChannels.initChannels() observeRenderableUnreadEventsUseCase() - .onStart { notificationChannels.initChannels() } .onEach { (each, diff) -> renderUnreadChange(each, diff) } - .collect() + .launchIn(scope) + + observeInviteNotificationsUseCase() + .onEach { inviteRenderer.render(it) } + .launchIn(scope) } private suspend fun renderUnreadChange(allUnread: Map>, diff: NotificationDiff) { diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt index 60d5e0e..014cce6 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt @@ -16,6 +16,9 @@ import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset private const val A_CHANNEL_ID = "a channel id" private val AN_OPEN_APP_INTENT = aPendingIntent() @@ -38,6 +41,7 @@ class NotificationFactoryTest { private val fakeNotificationStyleFactory = FakeNotificationStyleFactory() private val fakeIntentFactory = FakeIntentFactory() private val fakeIconLoader = FakeIconLoader() + private val fixedClock = Clock.fixed(Instant.ofEpochMilli(0), ZoneOffset.UTC) private val notificationFactory = NotificationFactory( fakeContext.instance, @@ -45,6 +49,7 @@ class NotificationFactoryTest { fakeIntentFactory, fakeIconLoader, DeviceMeta(26), + fixedClock ) @Test @@ -127,6 +132,30 @@ class NotificationFactoryTest { ) } + @Test + fun `given invite, then creates expected`() { + fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT) + val content = "Content message" + val result = notificationFactory.createInvite( + InviteNotification( + content = content, + A_ROOM_ID, + ) + ) + + result shouldBeEqualTo AndroidNotification( + channelId = INVITE_CHANNEL_ID, + whenTimestamp = fixedClock.millis(), + alertMoreThanOnce = true, + smallIcon = R.drawable.ic_notification_small_icon, + contentIntent = AN_OPEN_APP_INTENT, + category = Notification.CATEGORY_EVENT, + autoCancel = true, + contentTitle = "Invite", + contentText = content, + ) + } + private fun givenEventsFor(roomOverview: RoomOverview) { fakeIntentFactory.givenNotificationOpenMessage(fakeContext.instance, roomOverview.roomId).returns(AN_OPEN_ROOM_INTENT) fakeNotificationStyleFactory.givenMessage(EVENTS.sortedBy { it.utcTimestamp }, roomOverview).returns(A_NOTIFICATION_STYLE) diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt index ed128f2..0be9e2f 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt @@ -35,7 +35,7 @@ class NotificationRendererTest { private val fakeNotificationFactory = FakeNotificationFactory() private val fakeAndroidNotificationBuilder = FakeAndroidNotificationBuilder() - private val notificationRenderer = NotificationRenderer( + private val notificationRenderer = NotificationMessageRenderer( fakeNotificationManager.instance, fakeNotificationFactory.instance, fakeAndroidNotificationBuilder.instance, diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt index 6cc7836..5175956 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt @@ -1,9 +1,9 @@ package app.dapk.st.notifications -import fake.FakeNotificationChannels -import fake.FakeNotificationRenderer -import fake.FakeObserveUnreadNotificationsUseCase +import fake.* import fixture.NotificationDiffFixtures.aNotificationDiff +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Test import test.expect @@ -12,35 +12,41 @@ private val AN_UNREAD_NOTIFICATIONS = UnreadNotifications(emptyMap(), aNotificat class RenderNotificationsUseCaseTest { - private val fakeNotificationRenderer = FakeNotificationRenderer() + private val fakeNotificationMessageRenderer = FakeNotificationMessageRenderer() + private val fakeNotificationInviteRenderer = FakeNotificationInviteRenderer() private val fakeObserveUnreadNotificationsUseCase = FakeObserveUnreadNotificationsUseCase() + private val fakeObserveInviteNotificationsUseCase = FakeObserveInviteNotificationsUseCase() private val fakeNotificationChannels = FakeNotificationChannels().also { it.instance.expect { it.initChannels() } } private val renderNotificationsUseCase = RenderNotificationsUseCase( - fakeNotificationRenderer.instance, + fakeNotificationMessageRenderer.instance, + fakeNotificationInviteRenderer.instance, fakeObserveUnreadNotificationsUseCase, + fakeObserveInviteNotificationsUseCase, fakeNotificationChannels.instance, ) @Test fun `given events, when listening for changes then initiates channels once`() = runTest { - fakeNotificationRenderer.instance.expect { it.render(any()) } + fakeNotificationMessageRenderer.instance.expect { it.render(any()) } fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS) + fakeObserveInviteNotificationsUseCase.given().emits() - renderNotificationsUseCase.listenForNotificationChanges() + renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher())) fakeNotificationChannels.verifyInitiated() } @Test fun `given renderable unread events, when listening for changes, then renders change`() = runTest { - fakeNotificationRenderer.instance.expect { it.render(any()) } + fakeNotificationMessageRenderer.instance.expect { it.render(any()) } fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS) + fakeObserveInviteNotificationsUseCase.given().emits() - renderNotificationsUseCase.listenForNotificationChanges() + renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher())) - fakeNotificationRenderer.verifyRenders(AN_UNREAD_NOTIFICATIONS) + fakeNotificationMessageRenderer.verifyRenders(AN_UNREAD_NOTIFICATIONS) } } diff --git a/features/notifications/src/test/kotlin/fake/FakeNotificationInviteRenderer.kt b/features/notifications/src/test/kotlin/fake/FakeNotificationInviteRenderer.kt new file mode 100644 index 0000000..aa9c662 --- /dev/null +++ b/features/notifications/src/test/kotlin/fake/FakeNotificationInviteRenderer.kt @@ -0,0 +1,8 @@ +package fake + +import app.dapk.st.notifications.NotificationInviteRenderer +import io.mockk.mockk + +class FakeNotificationInviteRenderer { + val instance = mockk() +} \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/fake/FakeNotificationRenderer.kt b/features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt similarity index 81% rename from features/notifications/src/test/kotlin/fake/FakeNotificationRenderer.kt rename to features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt index fcdc118..288d694 100644 --- a/features/notifications/src/test/kotlin/fake/FakeNotificationRenderer.kt +++ b/features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt @@ -1,13 +1,13 @@ package fake -import app.dapk.st.notifications.NotificationRenderer +import app.dapk.st.notifications.NotificationMessageRenderer import app.dapk.st.notifications.NotificationState import app.dapk.st.notifications.UnreadNotifications import io.mockk.coVerify import io.mockk.mockk -class FakeNotificationRenderer { - val instance = mockk() +class FakeNotificationMessageRenderer { + val instance = mockk() fun verifyRenders(vararg unreadNotifications: UnreadNotifications) { unreadNotifications.forEach { unread -> @@ -23,4 +23,4 @@ class FakeNotificationRenderer { } } } -} \ No newline at end of file +} diff --git a/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt b/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt new file mode 100644 index 0000000..fba079f --- /dev/null +++ b/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt @@ -0,0 +1,10 @@ +package fake + +import app.dapk.st.notifications.ObserveInviteNotificationsUseCase +import io.mockk.coEvery +import io.mockk.mockk +import test.delegateEmit + +class FakeObserveInviteNotificationsUseCase : ObserveInviteNotificationsUseCase by mockk() { + fun given() = coEvery { this@FakeObserveInviteNotificationsUseCase.invoke() }.delegateEmit() +} \ No newline at end of file diff --git a/test-harness/src/test/kotlin/test/TestMatrix.kt b/test-harness/src/test/kotlin/test/TestMatrix.kt index b758207..590056e 100644 --- a/test-harness/src/test/kotlin/test/TestMatrix.kt +++ b/test-harness/src/test/kotlin/test/TestMatrix.kt @@ -164,7 +164,8 @@ class TestMatrix( ) } } - } + }, + roomInviteRemover = { storeModule.overviewStore().removeInvites(listOf(it)) } ) installSyncService( diff --git a/tools/coverage.gradle b/tools/coverage.gradle index 009e835..c4685d5 100644 --- a/tools/coverage.gradle +++ b/tools/coverage.gradle @@ -74,3 +74,12 @@ task allCodeCoverageReport(type: JacocoReport) { dependsOn { ["app:assembleDebug"] + projects*.test } initializeReport(it, projects, excludes) } + +task unitTestCodeCoverageReport(type: JacocoReport) { + outputs.upToDateWhen { false } + rootProject.apply plugin: 'jacoco' + def projects = collectProjects { !it.name.contains("test-harness") && !it.name.contains("stub") && !it.name.contains("-noop") } + dependsOn { ["app:assembleDebug"] + projects*.test } + initializeReport(it, projects, excludes) +} + From 831036fd43478358ad41ab04e421258f5fcfaba1 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 11 Sep 2022 21:53:00 +0100 Subject: [PATCH 08/16] only showing material you option for android 12+ devices --- .../kotlin/app/dapk/st/graph/AppModule.kt | 6 ++++- .../app/dapk/st/core/DeviceMetaExtensions.kt | 2 ++ .../dapk/st/settings/SettingsItemFactory.kt | 26 +++++++++++++++---- .../app/dapk/st/settings/SettingsModule.kt | 8 +++--- .../st/settings/SettingsItemFactoryTest.kt | 4 ++- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index ff26c53..12e237f 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -69,6 +69,7 @@ import java.time.Clock internal class AppModule(context: Application, logger: MatrixLogger) { private val buildMeta = BuildMeta(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE) + private val deviceMeta = DeviceMeta(Build.VERSION.SDK_INT) private val trackingModule by unsafeLazy { TrackingModule( isCrashTrackingEnabled = !BuildConfig.DEBUG @@ -135,6 +136,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) { imageLoaderModule, context, buildMeta, + deviceMeta, coroutineDispatchers, clock, ) @@ -149,6 +151,7 @@ internal class FeatureModules internal constructor( imageLoaderModule: ImageLoaderModule, context: Context, buildMeta: BuildMeta, + deviceMeta: DeviceMeta, coroutineDispatchers: CoroutineDispatchers, clock: Clock, ) { @@ -190,6 +193,7 @@ internal class FeatureModules internal constructor( matrixModules.sync, context.contentResolver, buildMeta, + deviceMeta, coroutineDispatchers, coreAndroidModule.themeStore(), ) @@ -202,7 +206,7 @@ internal class FeatureModules internal constructor( context, intentFactory = coreAndroidModule.intentFactory(), dispatchers = coroutineDispatchers, - deviceMeta = DeviceMeta(Build.VERSION.SDK_INT) + deviceMeta = deviceMeta ) } diff --git a/domains/android/core/src/main/kotlin/app/dapk/st/core/DeviceMetaExtensions.kt b/domains/android/core/src/main/kotlin/app/dapk/st/core/DeviceMetaExtensions.kt index 3cc7e00..a2dc53e 100644 --- a/domains/android/core/src/main/kotlin/app/dapk/st/core/DeviceMetaExtensions.kt +++ b/domains/android/core/src/main/kotlin/app/dapk/st/core/DeviceMetaExtensions.kt @@ -6,6 +6,8 @@ fun DeviceMeta.isAtLeastO(block: () -> T, fallback: () -> T = { throw Illega return if (this.apiVersion >= Build.VERSION_CODES.O) block() else fallback() } +fun DeviceMeta.isAtLeastS() = this.apiVersion >= Build.VERSION_CODES.S + fun DeviceMeta.onAtLeastO(block: () -> Unit) { if (this.apiVersion >= Build.VERSION_CODES.O) block() } diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsItemFactory.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsItemFactory.kt index 015c5cf..a36e2f1 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsItemFactory.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsItemFactory.kt @@ -1,26 +1,42 @@ package app.dapk.st.settings -import app.dapk.st.core.BuildMeta -import app.dapk.st.core.ThemeStore +import app.dapk.st.core.* import app.dapk.st.push.PushTokenRegistrars internal class SettingsItemFactory( private val buildMeta: BuildMeta, + private val deviceMeta: DeviceMeta, private val pushTokenRegistrars: PushTokenRegistrars, private val themeStore: ThemeStore, ) { - suspend fun root() = listOf( + suspend fun root() = general() + theme() + data() + account() + about() + + private suspend fun general() = listOf( SettingItem.Header("General"), SettingItem.Text(SettingItem.Id.Encryption, "Encryption"), SettingItem.Text(SettingItem.Id.EventLog, "Event log"), - SettingItem.Text(SettingItem.Id.PushProvider, "Push provider", pushTokenRegistrars.currentSelection().id), + SettingItem.Text(SettingItem.Id.PushProvider, "Push provider", pushTokenRegistrars.currentSelection().id) + ) + + private fun theme() = listOfNotNull( SettingItem.Header("Theme"), - SettingItem.Toggle(SettingItem.Id.ToggleDynamicTheme, "Enable Material You", state = themeStore.isMaterialYouEnabled()), + SettingItem.Toggle(SettingItem.Id.ToggleDynamicTheme, "Enable Material You", state = themeStore.isMaterialYouEnabled()).takeIf { + deviceMeta.isAtLeastS() + }, + ).takeIf { it.size > 1 } ?: emptyList() + + private fun data() = listOf( SettingItem.Header("Data"), SettingItem.Text(SettingItem.Id.ClearCache, "Clear cache"), + ) + + private fun account() = listOf( SettingItem.Header("Account"), SettingItem.Text(SettingItem.Id.SignOut, "Sign out"), + ) + + private fun about() = listOf( SettingItem.Header("About"), SettingItem.Text(SettingItem.Id.PrivacyPolicy, "Privacy policy"), SettingItem.Text(SettingItem.Id.Ignored, "Version", buildMeta.versionName), diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt index 9240eb0..4e24052 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt @@ -1,10 +1,7 @@ package app.dapk.st.settings import android.content.ContentResolver -import app.dapk.st.core.BuildMeta -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.ProvidableModule -import app.dapk.st.core.ThemeStore +import app.dapk.st.core.* import app.dapk.st.domain.StoreModule import app.dapk.st.matrix.crypto.CryptoService import app.dapk.st.matrix.sync.SyncService @@ -18,6 +15,7 @@ class SettingsModule( private val syncService: SyncService, private val contentResolver: ContentResolver, private val buildMeta: BuildMeta, + private val deviceMeta: DeviceMeta, private val coroutineDispatchers: CoroutineDispatchers, private val themeStore: ThemeStore, ) : ProvidableModule { @@ -29,7 +27,7 @@ class SettingsModule( cryptoService, syncService, UriFilenameResolver(contentResolver, coroutineDispatchers), - SettingsItemFactory(buildMeta, pushModule.pushTokenRegistrars(), themeStore), + SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore), pushModule.pushTokenRegistrars(), themeStore, ) diff --git a/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsItemFactoryTest.kt b/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsItemFactoryTest.kt index 95ffa3c..ab631c4 100644 --- a/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsItemFactoryTest.kt +++ b/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsItemFactoryTest.kt @@ -1,6 +1,7 @@ package app.dapk.st.settings import app.dapk.st.core.BuildMeta +import app.dapk.st.core.DeviceMeta import app.dapk.st.push.PushTokenRegistrars import app.dapk.st.push.Registrar import internalfixture.aSettingHeaderItem @@ -18,10 +19,11 @@ private const val ENABLED_MATERIAL_YOU = true class SettingsItemFactoryTest { private val buildMeta = BuildMeta(versionName = "a-version-name", versionCode = 100) + private val deviceMeta = DeviceMeta(apiVersion = 31) private val fakePushTokenRegistrars = FakePushRegistrars() private val fakeThemeStore = FakeThemeStore() - private val settingsItemFactory = SettingsItemFactory(buildMeta, fakePushTokenRegistrars.instance, fakeThemeStore.instance) + private val settingsItemFactory = SettingsItemFactory(buildMeta, deviceMeta, fakePushTokenRegistrars.instance, fakeThemeStore.instance) @Test fun `when creating root items, then is expected`() = runTest { From 4d0e2a9d99514b6e27ef0cefc594b0a124b2986b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 11 Sep 2022 22:26:20 +0100 Subject: [PATCH 09/16] skipping first invite emission as we only want to render future invites --- .../dapk/st/notifications/ObserveInviteNotificationsUseCase.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt index 802fb24..67d88c5 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt @@ -14,6 +14,7 @@ class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewS override suspend fun invoke(): Flow { return overviewStore.latestInvites() .diff() + .drop(1) .flatten() .map { val text = when (val meta = it.inviteMeta) { From fdd1ca61e87c248093a859df20ca89d4362e4b15 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 11 Sep 2022 22:34:03 +0100 Subject: [PATCH 10/16] fixing crash when switching from the invitations screen to the conversation directory and back to the profile --- .../main/kotlin/app/dapk/st/home/HomeViewModel.kt | 12 +++++++++++- .../main/kotlin/app/dapk/st/profile/ProfileScreen.kt | 5 +++-- .../kotlin/app/dapk/st/profile/ProfileViewModel.kt | 12 ++++++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt index 334660b..56617f7 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt @@ -98,7 +98,17 @@ class HomeViewModel( fun changePage(page: Page) { state = when (val current = state) { Loading -> current - is SignedIn -> current.copy(page = page) + is SignedIn -> { + when (page) { + Page.Directory -> { + // do nothing + } + + Page.Profile -> profileViewModel.reset() + } + current.copy(page = page) + } + SignedOut -> current } } diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt index af305b8..6d57470 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt @@ -7,10 +7,10 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CameraAlt import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -19,7 +19,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import app.dapk.st.core.Lce import app.dapk.st.core.LifecycleEffect -import app.dapk.st.core.StartObserving import app.dapk.st.core.components.CenteredLoading import app.dapk.st.design.components.* import app.dapk.st.matrix.sync.InviteMeta @@ -43,6 +42,7 @@ fun ProfileScreen(viewModel: ProfileViewModel, onTopLevelBack: () -> Unit) { else -> viewModel.goTo(it) } } + Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) { item(Page.Routes.profile) { ProfilePage(context, viewModel, it) @@ -146,6 +146,7 @@ private fun Invitations(viewModel: ProfileViewModel, invitations: Page.Invitatio } } } + is Lce.Error -> TODO() } } diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt index ab7fb5b..bec102a 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt @@ -102,10 +102,6 @@ class ProfileViewModel( ) } - fun stop() { - syncingJob?.cancel() - } - @Suppress("UNCHECKED_CAST") private inline fun updatePageState(crossinline block: S.() -> S) { val page = state.page @@ -114,6 +110,14 @@ class ProfileViewModel( updateState { copy(page = (page as SpiderPage).copy(state = block(page.state))) } } + fun reset() { + updateState { ProfileScreenState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false)) } + } + + fun stop() { + syncingJob?.cancel() + } + } fun DapkViewModel.launchCatching(block: suspend () -> T): LaunchCatching { From ee3d41a741e0e88260e1ab48d1ce194fe51b46d7 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 11 Sep 2022 22:48:06 +0100 Subject: [PATCH 11/16] fixing wrong initial push registrar selection --- .../src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt index 44a9679..eb29104 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt @@ -55,7 +55,7 @@ class PushTokenRegistrars( } override suspend fun registerCurrentToken() { - when (selection) { + when (currentSelection()) { FIREBASE_OPTION -> messagingPushTokenRegistrar.registerCurrentToken() NONE -> { // do nothing @@ -75,6 +75,10 @@ class PushTokenRegistrars( } } + null -> { + // do nothing + } + else -> unifiedPushRegistrar.unregister() } } From c6431add42ef878371dd36bad7faa6d26debf00c Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 12 Sep 2022 19:45:00 +0100 Subject: [PATCH 12/16] adding back a light theme or make use of the dynamic colour scheme --- .../app/dapk/st/design/components/Theme.kt | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Theme.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Theme.kt index 45259ba..3d8fd4d 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/Theme.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Theme.kt @@ -1,8 +1,7 @@ package app.dapk.st.design.components -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -18,13 +17,16 @@ private val DARK_COLOURS = darkColorScheme( onPrimary = Color(0xDDFFFFFF), ) -private val DARK_EXTENDED = createExtended(DARK_COLOURS.primary, DARK_COLOURS.onPrimary) +private val LIGHT_COLOURS = lightColorScheme( + primary = Palette.brandPrimary, + onPrimary = Color(0xDDFFFFFF), +) private fun createExtended(primary: Color, onPrimary: Color) = ExtendedColors( selfBubble = primary, onSelfBubble = onPrimary, othersBubble = Color(0x20EDEDED), - onOthersBubble = DARK_COLOURS.onPrimary, + onOthersBubble = onPrimary, selfBubbleReplyBackground = Color(0x40EAEAEA), otherBubbleReplyBackground = Color(0x20EAEAEA), missingImageColors = listOf( @@ -49,15 +51,23 @@ data class ExtendedColors( } } -private val LocalExtendedColors = staticCompositionLocalOf { DARK_EXTENDED } +private val LocalExtendedColors = staticCompositionLocalOf { throw IllegalAccessError() } @Composable fun SmallTalkTheme(themeConfig: ThemeConfig, content: @Composable () -> Unit) { val systemUiController = rememberSystemUiController() + val systemInDarkTheme = isSystemInDarkTheme() + val colorScheme = if (themeConfig.useDynamicTheme) { - dynamicDarkColorScheme(LocalContext.current) + when (systemInDarkTheme) { + true -> dynamicDarkColorScheme(LocalContext.current) + false -> dynamicLightColorScheme(LocalContext.current) + } } else { - DARK_COLOURS + when (systemInDarkTheme) { + true -> DARK_COLOURS + false -> LIGHT_COLOURS + } } MaterialTheme(colorScheme = colorScheme) { val backgroundColor = MaterialTheme.colorScheme.background From 62c0945be6b5da35a7ba94eb8ee2bd732bb03548 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 12 Sep 2022 20:06:16 +0100 Subject: [PATCH 13/16] cleaning up messages theming --- .../dapk/st/design/components/OverflowMenu.kt | 10 ++++-- .../app/dapk/st/design/components/Theme.kt | 36 +++++++++++-------- .../app/dapk/st/messenger/MessengerScreen.kt | 18 +++++----- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/OverflowMenu.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/OverflowMenu.kt index 6fe0965..31674e0 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/OverflowMenu.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/OverflowMenu.kt @@ -1,12 +1,15 @@ package app.dapk.st.design.components +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.DropdownMenu import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* +import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp @@ -18,7 +21,8 @@ fun OverflowMenu(content: @Composable () -> Unit) { DropdownMenu( expanded = showMenu, onDismissRequest = { showMenu = false }, - offset = DpOffset(0.dp, (-72).dp) + offset = DpOffset(0.dp, (-72).dp), + modifier = Modifier.background(MaterialTheme.colorScheme.secondaryContainer) ) { content() } diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Theme.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Theme.kt index 3d8fd4d..c1e37c2 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/Theme.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Theme.kt @@ -15,18 +15,22 @@ private object Palette { private val DARK_COLOURS = darkColorScheme( primary = Palette.brandPrimary, onPrimary = Color(0xDDFFFFFF), + secondaryContainer = Color(0xFF363639), + onSecondaryContainer = Color(0xDDFFFFFF), ) private val LIGHT_COLOURS = lightColorScheme( primary = Palette.brandPrimary, onPrimary = Color(0xDDFFFFFF), + secondaryContainer = Color(0xFFf1f0f1), + onSecondaryContainer = Color(0xFF000000), ) -private fun createExtended(primary: Color, onPrimary: Color) = ExtendedColors( - selfBubble = primary, - onSelfBubble = onPrimary, - othersBubble = Color(0x20EDEDED), - onOthersBubble = onPrimary, +private fun createExtended(scheme: ColorScheme) = ExtendedColors( + selfBubble = scheme.primary, + onSelfBubble = scheme.onPrimary, + othersBubble = scheme.secondaryContainer, + onOthersBubble = scheme.onSecondaryContainer, selfBubbleReplyBackground = Color(0x40EAEAEA), otherBubbleReplyBackground = Color(0x20EAEAEA), missingImageColors = listOf( @@ -58,15 +62,19 @@ fun SmallTalkTheme(themeConfig: ThemeConfig, content: @Composable () -> Unit) { val systemUiController = rememberSystemUiController() val systemInDarkTheme = isSystemInDarkTheme() - val colorScheme = if (themeConfig.useDynamicTheme) { - when (systemInDarkTheme) { - true -> dynamicDarkColorScheme(LocalContext.current) - false -> dynamicLightColorScheme(LocalContext.current) + val colorScheme = when { + themeConfig.useDynamicTheme -> { + when (systemInDarkTheme) { + true -> dynamicDarkColorScheme(LocalContext.current) + false -> dynamicLightColorScheme(LocalContext.current) + } } - } else { - when (systemInDarkTheme) { - true -> DARK_COLOURS - false -> LIGHT_COLOURS + + else -> { + when (systemInDarkTheme) { + true -> DARK_COLOURS + false -> LIGHT_COLOURS + } } } MaterialTheme(colorScheme = colorScheme) { @@ -74,7 +82,7 @@ fun SmallTalkTheme(themeConfig: ThemeConfig, content: @Composable () -> Unit) { SideEffect { systemUiController.setSystemBarsColor(backgroundColor) } - CompositionLocalProvider(LocalExtendedColors provides createExtended(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.onPrimary)) { + CompositionLocalProvider(LocalExtendedColors provides createExtended(colorScheme)) { content() } } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index 184d3d0..93e0a62 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -10,11 +10,11 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Send +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -64,7 +64,7 @@ internal fun MessengerScreen(roomId: RoomId, attachments: List Un .align(Alignment.Bottom) .weight(1f) .fillMaxHeight() - .background(Color.DarkGray, RoundedCornerShape(24.dp)), + .background(SmallTalkTheme.extendedColors.othersBubble, RoundedCornerShape(24.dp)), contentAlignment = Alignment.TopStart, ) { Box(Modifier.padding(14.dp)) { if (state.value.isEmpty()) { - Text("Message", color = SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.4f)) + Text("Message", color = SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.5f)) } BasicTextField( modifier = Modifier.fillMaxWidth(), @@ -584,11 +584,12 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un } Spacer(modifier = Modifier.width(6.dp)) var size by remember { mutableStateOf(IntSize(0, 0)) } + val enabled = state.value.isNotEmpty() IconButton( - enabled = state.value.isNotEmpty(), + enabled = enabled, modifier = Modifier .clip(CircleShape) - .background(if (state.value.isEmpty()) Color.DarkGray else MaterialTheme.colorScheme.primary) + .background(if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondaryContainer) .run { if (size.height == 0 || size.width == 0) { this @@ -607,7 +608,7 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un Icon( imageVector = Icons.Filled.Send, contentDescription = "", - tint = MaterialTheme.colorScheme.onPrimary, + tint = if (enabled) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.5f), ) } } @@ -633,7 +634,8 @@ private fun AttachmentComposer(state: ComposerState.Attachments, onSend: () -> U Box( Modifier .align(Alignment.BottomEnd) - .padding(12.dp)) { + .padding(12.dp) + ) { IconButton( enabled = true, modifier = Modifier From e854e9b02c36d6129a6d56a304713d8391180761 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 12 Sep 2022 20:57:33 +0100 Subject: [PATCH 14/16] removing top bar under status bar --- .../src/main/kotlin/app/dapk/st/design/components/Toolbar.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Toolbar.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Toolbar.kt index 0698301..dbcb390 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/Toolbar.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Toolbar.kt @@ -1,17 +1,14 @@ package app.dapk.st.design.components import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -31,7 +28,6 @@ fun Toolbar( title = title?.let { { Text(it, maxLines = 2) } } ?: {}, actions = actions, ) - Divider(modifier = Modifier.fillMaxWidth(), color = Color.Black.copy(alpha = 0.2f), thickness = 0.5.dp) } private fun foo(onNavigate: (() -> Unit)?): (@Composable () -> Unit) { From 2bf0ef2232656ba6e04c9a3694842768b499c656 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 13 Sep 2022 19:40:21 +0100 Subject: [PATCH 15/16] allowing unified push payloads to have default nulls - fixes crash on non message unified push events --- .../dapk/st/push/unifiedpush/UnifiedPushMessageReceiver.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageReceiver.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageReceiver.kt index 011b0dc..5a9176a 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageReceiver.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageReceiver.kt @@ -70,8 +70,8 @@ class UnifiedPushMessageReceiver : MessagingReceiver() { @Serializable data class Notification( - @SerialName("event_id") val eventId: String?, - @SerialName("room_id") val roomId: String?, + @SerialName("event_id") val eventId: String? = null, + @SerialName("room_id") val roomId: String? = null, ) } } \ No newline at end of file From 5f3511d619195512cc6506c5dfdbfc74efcb7c8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Sep 2022 05:24:33 +0000 Subject: [PATCH 16/16] Bump turbine from 0.9.0 to 0.10.0 Bumps [turbine](https://github.com/cashapp/turbine) from 0.9.0 to 0.10.0. - [Release notes](https://github.com/cashapp/turbine/releases) - [Changelog](https://github.com/cashapp/turbine/blob/trunk/CHANGELOG.md) - [Commits](https://github.com/cashapp/turbine/compare/0.9.0...0.10.0) --- updated-dependencies: - dependency-name: app.cash.turbine:turbine dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- test-harness/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-harness/build.gradle b/test-harness/build.gradle index 4664053..e694fdd 100644 --- a/test-harness/build.gradle +++ b/test-harness/build.gradle @@ -9,7 +9,7 @@ test { dependencies { kotlinTest(it) - testImplementation 'app.cash.turbine:turbine:0.9.0' + testImplementation 'app.cash.turbine:turbine:0.10.0' testImplementation Dependencies.mavenCentral.kotlinSerializationJson