From 04b6cf5375a6d22b0a6b5ab547add794a3dca9b2 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 30 Sep 2022 21:38:45 +0100 Subject: [PATCH 01/38] adding ability to show the raw exception information on login errors --- .../kotlin/app/dapk/st/login/LoginScreen.kt | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/features/login/src/main/kotlin/app/dapk/st/login/LoginScreen.kt b/features/login/src/main/kotlin/app/dapk/st/login/LoginScreen.kt index 81eb811..2271c9a 100644 --- a/features/login/src/main/kotlin/app/dapk/st/login/LoginScreen.kt +++ b/features/login/src/main/kotlin/app/dapk/st/login/LoginScreen.kt @@ -1,15 +1,16 @@ package app.dapk.st.login import android.widget.Toast +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.Web import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment @@ -49,10 +50,27 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) { when (val state = loginViewModel.state) { is Error -> { + val openDetailsDialog = remember { mutableStateOf(false) } + + if (openDetailsDialog.value) { + AlertDialog( + onDismissRequest = { openDetailsDialog.value = false }, + confirmButton = { + Button(onClick = { openDetailsDialog.value = false }) { + Text("OK") + } + }, + title = { Text("Details") }, + text = { + Text(state.cause.message ?: "Unknown") + } + ) + } Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Something went wrong") - Spacer(modifier = Modifier.height(6.dp)) + Text("Tap for more details".uppercase(), fontSize = 12.sp, modifier = Modifier.clickable { openDetailsDialog.value = true }.padding(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) Button(onClick = { loginViewModel.start() }) { @@ -61,11 +79,13 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) { } } } + Loading -> { Box(contentAlignment = Alignment.Center) { CircularProgressIndicator() } } + is Content -> Row { Spacer(modifier = Modifier.weight(0.1f)) From c05fe5ac257e3cefe81010585b6e38733d12051e Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 1 Oct 2022 10:14:06 +0100 Subject: [PATCH 02/38] allowing the profile call to continue when receiving a 404 instead of crashing - this case is known by the spec for when a profile doesn't exist yet - 403s still crash as fetching your own profile information should never result in access denied --- .../room/internal/DefaultProfileService.kt | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt index 5a39c53..7d4067e 100644 --- a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt +++ b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt @@ -5,6 +5,8 @@ import app.dapk.st.matrix.common.* import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.room.ProfileService import app.dapk.st.matrix.room.ProfileStore +import io.ktor.client.call.* +import io.ktor.client.plugins.* import kotlinx.coroutines.flow.first import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -29,12 +31,29 @@ internal class DefaultProfileService( private suspend fun fetchMe(): ProfileService.Me { val credentials = credentialsStore.credentials()!! val userId = credentials.userId - val result = httpClient.execute(profileRequest(userId)) - return ProfileService.Me( - userId, - result.displayName, - result.avatarUrl?.convertMxUrToUrl(credentials.homeServer)?.let { AvatarUrl(it) }, - homeServerUrl = credentials.homeServer, + return runCatching { httpClient.execute(profileRequest(userId)) }.fold( + onSuccess = { + ProfileService.Me( + userId, + it.displayName, + it.avatarUrl?.convertMxUrToUrl(credentials.homeServer)?.let { AvatarUrl(it) }, + homeServerUrl = credentials.homeServer, + ) + }, + onFailure = { + when { + it is ClientRequestException && it.response.status.value == 404 -> { + ProfileService.Me( + userId, + displayName = null, + avatarUrl = null, + homeServerUrl = credentials.homeServer, + ) + } + + else -> throw it + } + } ) } } From 7ca0224d7a0b0c6c5a40a4b8c07c77899f55be21 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 1 Oct 2022 10:40:32 +0100 Subject: [PATCH 03/38] extracting me fetching logic to its own usecase and testing --- .idea/inspectionProfiles/Project_Default.xml | 4 + .../kotlin/fake/FakeHttpResponse.kt | 16 ++++ .../kotlin/fake/FakeMatrixHttpClient.kt | 7 +- .../testFixtures/kotlin/fixture/HttpError.kt | 14 ++++ matrix/services/profile/build.gradle | 9 +++ .../app/dapk/st/matrix/room/ProfileService.kt | 6 +- .../room/internal/DefaultProfileService.kt | 47 +---------- .../st/matrix/room/internal/FetchMeUseCase.kt | 53 +++++++++++++ .../room/internal/FetchMeUseCaseTest.kt | 78 +++++++++++++++++++ 9 files changed, 186 insertions(+), 48 deletions(-) create mode 100644 matrix/matrix-http/src/testFixtures/kotlin/fake/FakeHttpResponse.kt create mode 100644 matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt create mode 100644 matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCase.kt create mode 100644 matrix/services/profile/src/test/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCaseTest.kt diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index bd2a505..2762fb3 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -4,6 +4,9 @@ + + @@ -25,5 +28,6 @@ + \ No newline at end of file diff --git a/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeHttpResponse.kt b/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeHttpResponse.kt new file mode 100644 index 0000000..fd08667 --- /dev/null +++ b/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeHttpResponse.kt @@ -0,0 +1,16 @@ +package fake + +import io.ktor.client.statement.* +import io.ktor.http.* +import io.mockk.every +import io.mockk.mockk + +class FakeHttpResponse { + + val instance = mockk(relaxed = true) + + fun givenStatus(code: Int) { + every { instance.status } returns HttpStatusCode(code, "") + } + +} \ No newline at end of file diff --git a/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeMatrixHttpClient.kt b/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeMatrixHttpClient.kt index ee97b55..1742bfd 100644 --- a/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeMatrixHttpClient.kt +++ b/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeMatrixHttpClient.kt @@ -1,6 +1,7 @@ package fake import app.dapk.st.matrix.http.MatrixHttpClient +import io.ktor.client.plugins.* import io.mockk.coEvery import io.mockk.mockk @@ -8,4 +9,8 @@ class FakeMatrixHttpClient : MatrixHttpClient by mockk() { fun given(request: MatrixHttpClient.HttpRequest, response: T) { coEvery { execute(request) } returns response } -} \ No newline at end of file + + fun errors(request: MatrixHttpClient.HttpRequest, cause: Throwable) { + coEvery { execute(request) } throws cause + } +} diff --git a/matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt b/matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt new file mode 100644 index 0000000..7722914 --- /dev/null +++ b/matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt @@ -0,0 +1,14 @@ +package fixture + +import fake.FakeHttpResponse +import io.ktor.client.plugins.* + +fun a404HttpError() = ClientRequestException( + FakeHttpResponse().apply { givenStatus(404) }.instance, + cachedResponseText = "" +) + +fun a403HttpError() = ClientRequestException( + FakeHttpResponse().apply { givenStatus(403) }.instance, + cachedResponseText = "" +) diff --git a/matrix/services/profile/build.gradle b/matrix/services/profile/build.gradle index eaa3259..6440905 100644 --- a/matrix/services/profile/build.gradle +++ b/matrix/services/profile/build.gradle @@ -1,5 +1,14 @@ +plugins { id 'java-test-fixtures' } applyMatrixServiceModule(project) dependencies { implementation project(":core") + + kotlinTest(it) + kotlinFixtures(it) + testImplementation(testFixtures(project(":matrix:common"))) + testImplementation(testFixtures(project(":matrix:matrix-http"))) + testImplementation(testFixtures(project(":core"))) + testFixturesImplementation(testFixtures(project(":matrix:common"))) + testFixturesImplementation(testFixtures(project(":core"))) } \ No newline at end of file diff --git a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileService.kt b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileService.kt index 73e4768..f5d6039 100644 --- a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileService.kt +++ b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileService.kt @@ -10,6 +10,7 @@ import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.HomeServerUrl import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.room.internal.DefaultProfileService +import app.dapk.st.matrix.room.internal.FetchMeUseCase private val SERVICE_KEY = ProfileService::class @@ -31,8 +32,9 @@ fun MatrixServiceInstaller.installProfileService( singletonFlows: SingletonFlows, credentialsStore: CredentialsStore, ): InstallExtender { - return this.install { (httpClient, _, _, logger) -> - SERVICE_KEY to DefaultProfileService(httpClient, logger, profileStore, singletonFlows, credentialsStore) + return this.install { (httpClient, _, _, _) -> + val fetchMeUseCase = FetchMeUseCase(httpClient, credentialsStore) + SERVICE_KEY to DefaultProfileService(profileStore, singletonFlows, fetchMeUseCase) } } diff --git a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt index 7d4067e..168808d 100644 --- a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt +++ b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt @@ -1,22 +1,14 @@ package app.dapk.st.matrix.room.internal import app.dapk.st.core.SingletonFlows -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.room.ProfileService import app.dapk.st.matrix.room.ProfileStore -import io.ktor.client.call.* -import io.ktor.client.plugins.* import kotlinx.coroutines.flow.first -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable internal class DefaultProfileService( - private val httpClient: MatrixHttpClient, - private val logger: MatrixLogger, private val profileStore: ProfileStore, private val singletonFlows: SingletonFlows, - private val credentialsStore: CredentialsStore, + private val fetchMeUseCase: FetchMeUseCase, ) : ProfileService { override suspend fun me(forceRefresh: Boolean): ProfileService.Me { @@ -29,42 +21,7 @@ internal class DefaultProfileService( } private suspend fun fetchMe(): ProfileService.Me { - val credentials = credentialsStore.credentials()!! - val userId = credentials.userId - return runCatching { httpClient.execute(profileRequest(userId)) }.fold( - onSuccess = { - ProfileService.Me( - userId, - it.displayName, - it.avatarUrl?.convertMxUrToUrl(credentials.homeServer)?.let { AvatarUrl(it) }, - homeServerUrl = credentials.homeServer, - ) - }, - onFailure = { - when { - it is ClientRequestException && it.response.status.value == 404 -> { - ProfileService.Me( - userId, - displayName = null, - avatarUrl = null, - homeServerUrl = credentials.homeServer, - ) - } - - else -> throw it - } - } - ) + return fetchMeUseCase.fetchMe() } } -internal fun profileRequest(userId: UserId) = MatrixHttpClient.HttpRequest.httpRequest( - path = "_matrix/client/r0/profile/${userId.value}/", - method = MatrixHttpClient.Method.GET, -) - -@Serializable -internal data class ApiMe( - @SerialName("displayname") val displayName: String? = null, - @SerialName("avatar_url") val avatarUrl: MxUrl? = null, -) \ No newline at end of file diff --git a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCase.kt b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCase.kt new file mode 100644 index 0000000..10401f3 --- /dev/null +++ b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCase.kt @@ -0,0 +1,53 @@ +package app.dapk.st.matrix.room.internal + +import app.dapk.st.matrix.common.* +import app.dapk.st.matrix.http.MatrixHttpClient +import app.dapk.st.matrix.room.ProfileService +import io.ktor.client.plugins.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +internal class FetchMeUseCase( + private val httpClient: MatrixHttpClient, + private val credentialsStore: CredentialsStore, +) { + suspend fun fetchMe(): ProfileService.Me { + val credentials = credentialsStore.credentials()!! + val userId = credentials.userId + return runCatching { httpClient.execute(profileRequest(userId)) }.fold( + onSuccess = { + ProfileService.Me( + userId, + it.displayName, + it.avatarUrl?.convertMxUrToUrl(credentials.homeServer)?.let { AvatarUrl(it) }, + homeServerUrl = credentials.homeServer, + ) + }, + onFailure = { + when { + it is ClientRequestException && it.response.status.value == 404 -> { + ProfileService.Me( + userId, + displayName = null, + avatarUrl = null, + homeServerUrl = credentials.homeServer, + ) + } + + else -> throw it + } + } + ) + } +} + +internal fun profileRequest(userId: UserId) = MatrixHttpClient.HttpRequest.httpRequest( + path = "_matrix/client/r0/profile/${userId.value}/", + method = MatrixHttpClient.Method.GET, +) + +@Serializable +internal data class ApiMe( + @SerialName("displayname") val displayName: String? = null, + @SerialName("avatar_url") val avatarUrl: MxUrl? = null, +) diff --git a/matrix/services/profile/src/test/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCaseTest.kt b/matrix/services/profile/src/test/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCaseTest.kt new file mode 100644 index 0000000..5ecf4a5 --- /dev/null +++ b/matrix/services/profile/src/test/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCaseTest.kt @@ -0,0 +1,78 @@ +package app.dapk.st.matrix.room.internal + +import app.dapk.st.matrix.room.ProfileService +import fake.FakeCredentialsStore +import fake.FakeMatrixHttpClient +import fixture.a403HttpError +import fixture.a404HttpError +import fixture.aUserCredentials +import fixture.aUserId +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.coInvoking +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldThrow +import org.junit.Test + +private val A_USER_CREDENTIALS = aUserCredentials() +private val A_USER_ID = aUserId() +private val AN_API_ME_RESPONSE = ApiMe( + displayName = "a display name", + avatarUrl = null, +) +private val AN_UNHANDLED_ERROR = RuntimeException() + +class FetchMeUseCaseTest { + + private val fakeHttpClient = FakeMatrixHttpClient() + private val fakeCredentialsStore = FakeCredentialsStore() + + private val useCase = FetchMeUseCase(fakeHttpClient, fakeCredentialsStore) + + @Test + fun `when fetching me, then returns Me instance`() = runTest { + fakeCredentialsStore.givenCredentials().returns(A_USER_CREDENTIALS) + fakeHttpClient.given(profileRequest(aUserId()), AN_API_ME_RESPONSE) + + val result = useCase.fetchMe() + + result shouldBeEqualTo ProfileService.Me( + userId = A_USER_ID, + displayName = AN_API_ME_RESPONSE.displayName, + avatarUrl = null, + homeServerUrl = fakeCredentialsStore.credentials()!!.homeServer + ) + } + + @Test + fun `given unhandled error, when fetching me, then throws`() = runTest { + fakeCredentialsStore.givenCredentials().returns(A_USER_CREDENTIALS) + fakeHttpClient.errors(profileRequest(aUserId()), AN_UNHANDLED_ERROR) + + coInvoking { useCase.fetchMe() } shouldThrow AN_UNHANDLED_ERROR + } + + @Test + fun `given 403, when fetching me, then throws`() = runTest { + val error = a403HttpError() + fakeCredentialsStore.givenCredentials().returns(A_USER_CREDENTIALS) + fakeHttpClient.errors(profileRequest(aUserId()), error) + + coInvoking { useCase.fetchMe() } shouldThrow error + } + + @Test + fun `given 404, when fetching me, then returns Me instance with empty profile fields`() = runTest { + fakeCredentialsStore.givenCredentials().returns(A_USER_CREDENTIALS) + fakeHttpClient.errors(profileRequest(aUserId()), a404HttpError()) + + val result = useCase.fetchMe() + + result shouldBeEqualTo ProfileService.Me( + userId = A_USER_ID, + displayName = null, + avatarUrl = null, + homeServerUrl = fakeCredentialsStore.credentials()!!.homeServer + ) + } + +} \ No newline at end of file From bce7a9854623305e4a8e53db699afb5b528f3611 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 30 Sep 2022 14:04:16 +0100 Subject: [PATCH 04/38] updating to latest material3 lib --- dependencies.gradle | 2 +- .../src/main/kotlin/app/dapk/st/design/components/Toolbar.kt | 2 +- .../src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt | 2 +- .../src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt | 5 ++++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 9f09a1f..7fef836 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -108,7 +108,7 @@ ext.Dependencies.with { androidxComposeUi = "androidx.compose.ui:ui:${composeVer}" androidxComposeFoundation = "androidx.compose.foundation:foundation:${composeVer}" - androidxComposeMaterial = "androidx.compose.material3:material3:1.0.0-beta01" + androidxComposeMaterial = "androidx.compose.material3:material3:1.0.0-beta03" androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}" androidxActivityCompose = "androidx.activity:activity-compose:1.4.0" kotlinCompilerExtensionVersion = "1.3.1" 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 dbcb390..ec0010e 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 @@ -19,7 +19,7 @@ fun Toolbar( actions: @Composable RowScope.() -> Unit = {} ) { val navigationIcon = foo(onNavigate) - SmallTopAppBar( + TopAppBar( modifier = offset?.let { Modifier.offset(it) } ?: Modifier, colors = TopAppBarDefaults.smallTopAppBarColors( containerColor = MaterialTheme.colorScheme.background diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt index 8762808..e84d5da 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt @@ -10,7 +10,7 @@ inline fun ComponentActivity.viewModel( ): Lazy { val factoryPromise = object : Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class) = when (modelClass) { + override fun create(modelClass: Class) = when (modelClass) { VM::class.java -> factory() as T else -> throw Error() } 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 ee891ff..9fe5472 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 @@ -37,7 +37,10 @@ import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.StartObserving import app.dapk.st.core.components.CenteredLoading import app.dapk.st.core.extensions.takeIfContent -import app.dapk.st.design.components.* +import app.dapk.st.design.components.MessengerUrlIcon +import app.dapk.st.design.components.MissingAvatarIcon +import app.dapk.st.design.components.SmallTalkTheme +import app.dapk.st.design.components.Toolbar import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.sync.MessageMeta From 6989c1399a870dfa7b27e17d5f324403406e2e7f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 30 Sep 2022 16:07:38 +0100 Subject: [PATCH 05/38] adding swiping to start reply process --- .../app/dapk/st/messenger/MessengerScreen.kt | 130 +++++++++++++----- .../app/dapk/st/messenger/MessengerState.kt | 5 + .../dapk/st/messenger/MessengerViewModel.kt | 43 +++++- 3 files changed, 139 insertions(+), 39 deletions(-) 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 9fe5472..aeb8ce9 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 @@ -2,10 +2,17 @@ package app.dapk.st.messenger import android.content.res.Configuration import androidx.activity.result.ActivityResultLauncher +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.foundation.shape.CircleShape @@ -53,6 +60,7 @@ import app.dapk.st.navigator.Navigator import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import kotlinx.coroutines.launch +import kotlin.math.roundToInt @Composable internal fun MessengerScreen( @@ -83,7 +91,9 @@ internal fun MessengerScreen( }) when (state.composerState) { is ComposerState.Text -> { - Room(state.roomState) + Room(state.roomState, onReply = { + viewModel.post(MessengerAction.ComposerEnterReplyMode(it)) + }) TextComposer( state.composerState, onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) }, @@ -119,11 +129,11 @@ private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLaun } @Composable -private fun ColumnScope.Room(roomStateLce: Lce) { +private fun ColumnScope.Room(roomStateLce: Lce, onReply: (RoomEvent) -> Unit) { when (val state = roomStateLce) { is Lce.Loading -> CenteredLoading() is Lce.Content -> { - RoomContent(state.value.self, state.value.roomState) + RoomContent(state.value.self, state.value.roomState, onReply) val eventBarHeight = 14.dp val typing = state.value.typing when { @@ -166,7 +176,7 @@ private fun ColumnScope.Room(roomStateLce: Lce) { } @Composable -private fun ColumnScope.RoomContent(self: UserId, state: RoomState) { +private fun ColumnScope.RoomContent(self: UserId, state: RoomState, onReply: (RoomEvent) -> Unit) { val listState: LazyListState = rememberLazyListState( initialFirstVisibleItemIndex = 0 ) @@ -192,7 +202,7 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState) { ) { index, item -> val previousEvent = if (index != 0) state.events[index - 1] else null val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id - AlignedBubble(item, self, wasPreviousMessageSameSender) { + AlignedBubble(item, self, wasPreviousMessageSameSender, onReply) { when (item) { is RoomEvent.Image -> MessageImage(it as BubbleContent) is Message -> TextBubbleContent(it as BubbleContent) @@ -215,6 +225,7 @@ private fun LazyItemScope.AlignedBubble( message: T, self: UserId, wasPreviousMessageSameSender: Boolean, + onReply: (RoomEvent) -> Unit, content: @Composable (BubbleContent) -> Unit ) { when (message.author.id == self) { @@ -224,7 +235,8 @@ private fun LazyItemScope.AlignedBubble( Bubble( message = message, isNotSelf = false, - wasPreviousMessageSameSender = wasPreviousMessageSameSender + wasPreviousMessageSameSender = wasPreviousMessageSameSender, + onReply = onReply, ) { content(BubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message)) } @@ -237,7 +249,8 @@ private fun LazyItemScope.AlignedBubble( Bubble( message = message, isNotSelf = true, - wasPreviousMessageSameSender = wasPreviousMessageSameSender + wasPreviousMessageSameSender = wasPreviousMessageSameSender, + onReply = onReply, ) { content(BubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message)) } @@ -332,9 +345,39 @@ private fun Bubble( message: RoomEvent, isNotSelf: Boolean, wasPreviousMessageSameSender: Boolean, + onReply: (RoomEvent) -> Unit, content: @Composable () -> Unit ) { - Row(Modifier.padding(horizontal = 12.dp)) { + + val screenWidthDp = LocalConfiguration.current.screenWidthDp.dp + val localDensity = LocalDensity.current + + val coroutineScope = rememberCoroutineScope() + val offsetX = remember { Animatable(0f) } + + Row( + Modifier.padding(horizontal = 12.dp) + .offset { IntOffset(offsetX.value.roundToInt(), 0) } + .draggable( + orientation = Orientation.Horizontal, + state = rememberDraggableState { + if ((offsetX.value + it) > 0) { + coroutineScope.launch { offsetX.snapTo(offsetX.value + it) } + } + }, + onDragStopped = { + with(localDensity) { + if (offsetX.value > (screenWidthDp.toPx() * 0.15)) { + onReply(message) + } + } + + coroutineScope.launch { + offsetX.animateTo(targetValue = 0f) + } + } + ) + ) { when { isNotSelf -> { val displayImageSize = 32.dp @@ -583,36 +626,57 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un .fillMaxWidth() .height(IntrinsicSize.Min), verticalAlignment = Alignment.Bottom ) { - Box( + Column( modifier = Modifier - .align(Alignment.Bottom) .weight(1f) .fillMaxHeight() - .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.5f)) - } - BasicTextField( - modifier = Modifier.fillMaxWidth(), - value = state.value, - onValueChange = { onTextChange(it) }, - cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - textStyle = LocalTextStyle.current.copy(color = SmallTalkTheme.extendedColors.onOthersBubble), - keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true), - decorationBox = { innerField -> - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Box(modifier = Modifier.weight(1f).padding(end = 4.dp)) { innerField() } - Icon( - modifier = Modifier.clickable { onAttach() }.wrapContentWidth().align(Alignment.Bottom), - imageVector = Icons.Filled.Image, - contentDescription = "", - ) - } +// AnimatedVisibility( +// visible = state.reply?.let { it is Message } ?: false, +// enter = slideInVertically { it - 50 }, +// exit = slideOutVertically { it - 50 }, +// ) { +// +// val message = state.reply as Message +// Column( +// modifier = Modifier +// .background(SmallTalkTheme.extendedColors.othersBubble, RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)) +// ) { +// Text(message.author.displayName ?: message.author.id.value) +// Text(message.content) +// Spacer(Modifier.height(50.dp)) +// } +// } + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .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.5f)) } - ) + BasicTextField( + modifier = Modifier.fillMaxWidth(), + value = state.value, + onValueChange = { onTextChange(it) }, + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + textStyle = LocalTextStyle.current.copy(color = SmallTalkTheme.extendedColors.onOthersBubble), + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true), + decorationBox = { innerField -> + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Box(modifier = Modifier.weight(1f).padding(end = 4.dp)) { innerField() } + Icon( + modifier = Modifier.clickable { onAttach() }.wrapContentWidth().align(Alignment.Bottom), + imageVector = Icons.Filled.Image, + contentDescription = "", + ) + } + } + ) + } } } Spacer(modifier = Modifier.width(6.dp)) diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt index cf335e6..7c0259c 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt @@ -2,6 +2,7 @@ package app.dapk.st.messenger import app.dapk.st.core.Lce import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.navigator.MessageAttachment data class MessengerScreenState( @@ -16,12 +17,16 @@ sealed interface MessengerEvent { sealed interface ComposerState { + val reply: RoomEvent? + data class Text( val value: String, + override val reply: RoomEvent?, ) : ComposerState data class Attachments( val values: List, + override val reply: RoomEvent?, ) : ComposerState } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt index 94a59f1..c533243 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt @@ -34,7 +34,7 @@ internal class MessengerViewModel( initialState = MessengerScreenState( roomId = null, roomState = Lce.Loading(), - composerState = ComposerState.Text(value = "") + composerState = ComposerState.Text(value = "", reply = null) ), factory = factory, ) { @@ -45,15 +45,40 @@ internal class MessengerViewModel( when (action) { is MessengerAction.OnMessengerVisible -> start(action) MessengerAction.OnMessengerGone -> syncJob?.cancel() - is MessengerAction.ComposerTextUpdate -> updateState { copy(composerState = ComposerState.Text(action.newValue)) } + is MessengerAction.ComposerTextUpdate -> updateState { copy(composerState = ComposerState.Text(action.newValue, composerState.reply)) } MessengerAction.ComposerSendText -> sendMessage() - MessengerAction.ComposerClear -> updateState { copy(composerState = ComposerState.Text("")) } - is MessengerAction.ComposerImageUpdate -> updateState { copy(composerState = ComposerState.Attachments(listOf(action.newValue))) } + MessengerAction.ComposerClear -> resetComposer() + is MessengerAction.ComposerImageUpdate -> updateState { + copy( + composerState = ComposerState.Attachments( + listOf(action.newValue), + composerState.reply + ) + ) + } + + is MessengerAction.ComposerEnterReplyMode -> updateState { + copy( + composerState = when (composerState) { + is ComposerState.Attachments -> composerState.copy(reply = action.replyingTo) + is ComposerState.Text -> composerState.copy(reply = action.replyingTo) + } + ) + } + + MessengerAction.ComposerExitReplyMode -> updateState { + copy( + composerState = when (composerState) { + is ComposerState.Attachments -> composerState.copy(reply = null) + is ComposerState.Text -> composerState.copy(reply = null) + } + ) + } } } private fun start(action: MessengerAction.OnMessengerVisible) { - updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it) } ?: composerState) } + updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it, null) } ?: composerState) } syncJob = viewModelScope.launch { roomStore.markRead(action.roomId) @@ -104,7 +129,7 @@ internal class MessengerViewModel( is ComposerState.Attachments -> { val copy = composerState.copy() - updateState { copy(composerState = ComposerState.Text("")) } + resetComposer() state.roomState.takeIfContent()?.let { content -> val roomState = content.roomState @@ -125,6 +150,10 @@ internal class MessengerViewModel( } } + private fun resetComposer() { + updateState { copy(composerState = ComposerState.Text("", reply = null)) } + } + fun startAttachment() { viewModelScope.launch { _events.emit(MessengerEvent.SelectImageAttachment) @@ -141,6 +170,8 @@ private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roo sealed interface MessengerAction { data class ComposerTextUpdate(val newValue: String) : MessengerAction + data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction + object ComposerExitReplyMode : MessengerAction data class ComposerImageUpdate(val newValue: MessageAttachment) : MessengerAction object ComposerSendText : MessengerAction object ComposerClear : MessengerAction From d03cadb3b761b5d04cd0cf3b370ac7275fb3fe34 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 30 Sep 2022 16:45:32 +0100 Subject: [PATCH 06/38] moving the hint state to the field decoration --- .../kotlin/app/dapk/st/messenger/MessengerScreen.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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 aeb8ce9..05c1186 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 @@ -2,10 +2,7 @@ package app.dapk.st.messenger import android.content.res.Configuration import androidx.activity.result.ActivityResultLauncher -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -655,9 +652,6 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un contentAlignment = Alignment.TopStart, ) { Box(Modifier.padding(14.dp)) { - if (state.value.isEmpty()) { - Text("Message", color = SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.5f)) - } BasicTextField( modifier = Modifier.fillMaxWidth(), value = state.value, @@ -667,7 +661,12 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true), decorationBox = { innerField -> Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Box(modifier = Modifier.weight(1f).padding(end = 4.dp)) { innerField() } + Box(modifier = Modifier.weight(1f).padding(end = 4.dp)) { + if (state.value.isEmpty()) { + Text("Message", color = SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.5f)) + } + innerField() + } Icon( modifier = Modifier.clickable { onAttach() }.wrapContentWidth().align(Alignment.Bottom), imageVector = Icons.Filled.Image, From 94a05898c808c675310a7a35c8267f0fca22a634 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 30 Sep 2022 16:46:19 +0100 Subject: [PATCH 07/38] flattening box layer --- .../app/dapk/st/messenger/MessengerScreen.kt | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) 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 05c1186..51bf4c3 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 @@ -651,31 +651,29 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un .background(SmallTalkTheme.extendedColors.othersBubble, RoundedCornerShape(24.dp)), contentAlignment = Alignment.TopStart, ) { - Box(Modifier.padding(14.dp)) { - BasicTextField( - modifier = Modifier.fillMaxWidth(), - value = state.value, - onValueChange = { onTextChange(it) }, - cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - textStyle = LocalTextStyle.current.copy(color = SmallTalkTheme.extendedColors.onOthersBubble), - keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true), - decorationBox = { innerField -> - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Box(modifier = Modifier.weight(1f).padding(end = 4.dp)) { - if (state.value.isEmpty()) { - Text("Message", color = SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.5f)) - } - innerField() + BasicTextField( + modifier = Modifier.fillMaxWidth().padding(14.dp), + value = state.value, + onValueChange = { onTextChange(it) }, + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + textStyle = LocalTextStyle.current.copy(color = SmallTalkTheme.extendedColors.onOthersBubble), + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true), + decorationBox = { innerField -> + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Box(modifier = Modifier.weight(1f).padding(end = 4.dp)) { + if (state.value.isEmpty()) { + Text("Message", color = SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.5f)) } - Icon( - modifier = Modifier.clickable { onAttach() }.wrapContentWidth().align(Alignment.Bottom), - imageVector = Icons.Filled.Image, - contentDescription = "", - ) + innerField() } + Icon( + modifier = Modifier.clickable { onAttach() }.wrapContentWidth().align(Alignment.Bottom), + imageVector = Icons.Filled.Image, + contentDescription = "", + ) } - ) - } + } + ) } } Spacer(modifier = Modifier.width(6.dp)) From 6cf0df7ea93ef27b2350a93ba55f07ef1b2d01df Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 30 Sep 2022 18:00:43 +0100 Subject: [PATCH 08/38] adding sliding reply animation --- .../app/dapk/st/messenger/MessengerScreen.kt | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) 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 51bf4c3..55f371b 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 @@ -2,7 +2,9 @@ package app.dapk.st.messenger import android.content.res.Configuration import androidx.activity.result.ActivityResultLauncher +import androidx.compose.animation.* import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -26,6 +28,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.onSizeChanged @@ -35,6 +38,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* +import androidx.compose.ui.zIndex import androidx.core.net.toUri import app.dapk.st.core.Lce import app.dapk.st.core.LifecycleEffect @@ -614,6 +618,7 @@ private fun RowScope.SendStatus(message: RoomEvent) { } } +@OptIn(ExperimentalAnimationApi::class) @Composable private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Unit, onSend: () -> Unit, onAttach: () -> Unit) { Row( @@ -623,34 +628,38 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un .fillMaxWidth() .height(IntrinsicSize.Min), verticalAlignment = Alignment.Bottom ) { + Column( modifier = Modifier .weight(1f) - .fillMaxHeight() + .background(SmallTalkTheme.extendedColors.othersBubble, RoundedCornerShape(24.dp)), ) { -// AnimatedVisibility( -// visible = state.reply?.let { it is Message } ?: false, -// enter = slideInVertically { it - 50 }, -// exit = slideOutVertically { it - 50 }, -// ) { -// -// val message = state.reply as Message -// Column( -// modifier = Modifier -// .background(SmallTalkTheme.extendedColors.othersBubble, RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)) -// ) { -// Text(message.author.displayName ?: message.author.id.value) -// Text(message.content) -// Spacer(Modifier.height(50.dp)) -// } -// } - Box( - modifier = Modifier - .weight(1f) - .fillMaxHeight() - .background(SmallTalkTheme.extendedColors.othersBubble, RoundedCornerShape(24.dp)), - contentAlignment = Alignment.TopStart, + AnimatedContent( + targetState = state.reply, + transitionSpec = { + slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Up, animationSpec = tween(500)){ + it / 2 + } + .with(slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Down)) + .using( + SizeTransform( + clip = true, + sizeAnimationSpec = { initialSize, targetSize -> + tween(500) + }) + + ) + } ) { + if (it is Message) { + Column(Modifier.clipToBounds()) { + Text(it.author.displayName ?: it.author.id.value) + Text(it.content, maxLines = 2) + } + } + } + + Box(modifier = Modifier.background(SmallTalkTheme.extendedColors.othersBubble, RoundedCornerShape(24.dp))) { BasicTextField( modifier = Modifier.fillMaxWidth().padding(14.dp), value = state.value, From 0034ddfe0a54a32ffb6ccc48f11ac9c143e869e3 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 30 Sep 2022 18:08:20 +0100 Subject: [PATCH 09/38] adding barebones styling to the reply box --- .../app/dapk/st/messenger/MessengerScreen.kt | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) 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 55f371b..3fa4259 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 @@ -38,7 +38,6 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* -import androidx.compose.ui.zIndex import androidx.core.net.toUri import app.dapk.st.core.Lce import app.dapk.st.core.LifecycleEffect @@ -637,7 +636,7 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un AnimatedContent( targetState = state.reply, transitionSpec = { - slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Up, animationSpec = tween(500)){ + slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Up, animationSpec = tween(500)) { it / 2 } .with(slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Down)) @@ -652,9 +651,30 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un } ) { if (it is Message) { - Column(Modifier.clipToBounds()) { - Text(it.author.displayName ?: it.author.id.value) - Text(it.content, maxLines = 2) + Column(Modifier.padding(12.dp)) { + Column( + Modifier + .fillMaxWidth() + .background(SmallTalkTheme.extendedColors.selfBubbleReplyBackground) + .padding(4.dp) + ) { + val replyName = it.author.displayName ?: it.author.id.value + Text( + fontSize = 11.sp, + text = replyName, + maxLines = 1, + color = SmallTalkTheme.extendedColors.onSelfBubble + ) + + Text( + text = it.content, + color = SmallTalkTheme.extendedColors.onSelfBubble, + fontSize = 15.sp, + maxLines = 2, + modifier = Modifier.wrapContentSize(), + textAlign = TextAlign.Start, + ) + } } } } From bccf9475087b43c3d19aa28018b15c4589b06a35 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 30 Sep 2022 20:17:22 +0100 Subject: [PATCH 10/38] ignoring nulls unless explicitly declared in the api models, passing null breaks some endpoints --- .../kotlin/app/dapk/st/matrix/http/MatrixHttpClient.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/MatrixHttpClient.kt b/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/MatrixHttpClient.kt index 85e5e7d..6cf64a5 100644 --- a/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/MatrixHttpClient.kt +++ b/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/MatrixHttpClient.kt @@ -3,6 +3,7 @@ package app.dapk.st.matrix.http import io.ktor.client.utils.* import io.ktor.http.content.* import io.ktor.util.reflect.* +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json interface MatrixHttpClient { @@ -47,7 +48,11 @@ interface MatrixHttpClient { companion object { val json = Json - val jsonWithDefaults = Json { encodeDefaults = true } + @OptIn(ExperimentalSerializationApi::class) + val jsonWithDefaults = Json { + encodeDefaults = true + explicitNulls = false + } } fun interface Factory { From 81f15c4fcaa9c5e3e210f5183129a1c912d22d99 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 30 Sep 2022 20:17:37 +0100 Subject: [PATCH 11/38] adding support for sending replies --- .../dapk/st/messenger/MessengerViewModel.kt | 14 ++++- .../dapk/st/matrix/message/MessageService.kt | 18 ++++-- .../st/matrix/message/internal/ApiMessage.kt | 23 +++++++- .../message/internal/SendMessageUseCase.kt | 55 ++++++++++++++++--- .../st/matrix/message/internal/SendRequest.kt | 40 ++++++++++++-- 5 files changed, 127 insertions(+), 23 deletions(-) diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt index c533243..a31b1b0 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt @@ -109,7 +109,7 @@ internal class MessengerViewModel( when (val composerState = state.composerState) { is ComposerState.Text -> { val copy = composerState.copy() - updateState { copy(composerState = composerState.copy(value = "")) } + updateState { copy(composerState = composerState.copy(value = "", reply = null)) } state.roomState.takeIfContent()?.let { content -> val roomState = content.roomState @@ -121,6 +121,18 @@ internal class MessengerViewModel( sendEncrypted = roomState.roomOverview.isEncrypted, localId = localIdFactory.create(), timestampUtc = clock.millis(), + reply = copy.reply?.let { + MessageService.Message.TextMessage.Reply( + authorId = it.author.id, + originalMessage = when (it) { + is RoomEvent.Image -> TODO() + is RoomEvent.Reply -> TODO() + is RoomEvent.Message -> it.content + }, + copy.value, + it.eventId + ) + } ) ) } diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt index 35b6297..2c2ff69 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt @@ -1,11 +1,7 @@ package app.dapk.st.matrix.message -import app.dapk.st.core.Base64 import app.dapk.st.matrix.* -import app.dapk.st.matrix.common.AlgorithmName -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.MessageType -import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.common.* import app.dapk.st.matrix.message.internal.DefaultMessageService import app.dapk.st.matrix.message.internal.ImageContentReader import kotlinx.coroutines.flow.Flow @@ -43,7 +39,17 @@ interface MessageService : MatrixService { @SerialName("room_id") val roomId: RoomId, @SerialName("local_id") val localId: String, @SerialName("timestamp") val timestampUtc: Long, - ) : Message() + @SerialName("reply") val reply: Reply? = null, + @SerialName("reply_id") val replyId: String? = null, + ) : Message() { + @Serializable + data class Reply( + val authorId: UserId, + val originalMessage: String, + val replyContent: String, + val eventId: EventId + ) + } @Serializable @SerialName("image_message") diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ApiMessage.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ApiMessage.kt index 84bc9f7..d71b6da 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ApiMessage.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ApiMessage.kt @@ -1,5 +1,6 @@ package app.dapk.st.matrix.message.internal +import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.MessageType import app.dapk.st.matrix.common.MxUrl import app.dapk.st.matrix.common.RoomId @@ -20,8 +21,26 @@ sealed class ApiMessage { @Serializable data class TextContent( @SerialName("body") val body: String, - @SerialName("msgtype") val type: String = MessageType.TEXT.value, - ) : ApiMessageContent + @SerialName("m.relates_to") val relatesTo: RelatesTo? = null, + @SerialName("formatted_body") val formattedBody: String? = null, + @SerialName("format") val format: String? = null, + ) : ApiMessageContent { + + @SerialName("msgtype") + val type: String = MessageType.TEXT.value + } + } + + @Serializable + data class RelatesTo( + @SerialName("m.in_reply_to") val inReplyTo: InReplyTo + ) { + + @Serializable + data class InReplyTo( + @SerialName("event_id") val eventId: EventId + ) + } @Serializable diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt index a75e088..aa573f2 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt @@ -1,9 +1,6 @@ package app.dapk.st.matrix.message.internal -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.EventType -import app.dapk.st.matrix.common.JsonString -import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.common.* import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest import app.dapk.st.matrix.message.ApiSendResponse @@ -37,7 +34,7 @@ internal class SendMessageUseCase( } private suspend fun ApiMessageMapper.textMessageRequest(message: Message.TextMessage): HttpRequest { - val contents = message.toContents() + val contents = message.toContents(message.reply) return when (message.sendEncrypted) { true -> sendRequest( roomId = message.roomId, @@ -49,6 +46,7 @@ internal class SendMessageUseCase( contents.toMessageJson(message.roomId) ) ), + relatesTo = contents.relatesTo ) false -> sendRequest( @@ -115,6 +113,7 @@ internal class SendMessageUseCase( eventType = EventType.ENCRYPTED, txId = message.localId, content = messageEncrypter.encrypt(MessageEncrypter.ClearMessagePayload(message.roomId, json)), + relatesTo = null ) } @@ -148,13 +147,22 @@ internal class SendMessageUseCase( } +private val MX_REPLY_REGEX = ".*".toRegex() class ApiMessageMapper { - fun Message.TextMessage.toContents() = ApiMessage.TextMessage.TextContent( - this.content.body, - this.content.type, - ) + fun Message.TextMessage.toContents(reply: Message.TextMessage.Reply?) = when (reply) { + null -> ApiMessage.TextMessage.TextContent( + body = this.content.body, + ) + + else -> ApiMessage.TextMessage.TextContent( + body = buildReplyFallback(reply.originalMessage, reply.authorId, reply.replyContent), + relatesTo = ApiMessage.RelatesTo(ApiMessage.RelatesTo.InReplyTo(reply.eventId)), + formattedBody = buildFormattedReply(reply.authorId, reply.originalMessage, reply.replyContent, this.roomId, reply.eventId), + format = "org.matrix.custom.html" + ) + } fun ApiMessage.TextMessage.TextContent.toMessageJson(roomId: RoomId) = JsonString( MatrixHttpClient.jsonWithDefaults.encodeToString( @@ -167,4 +175,33 @@ class ApiMessageMapper { ) ) + private fun buildReplyFallback(originalMessage: String, originalSenderId: UserId, reply: String): String { + return buildString { + append("> <") + append(originalSenderId.value) + append(">") + + val lines = originalMessage.split("\n") + lines.forEachIndexed { index, s -> + if (index == 0) { + append(" $s") + } else { + append("\n> $s") + } + } + append("\n\n") + append(reply) + } + } + + private fun buildFormattedReply(userId: UserId, originalMessage: String, reply: String, roomId: RoomId, eventId: EventId): String { + val permalink = "https://matrix.to/#/${roomId.value}/${eventId.value}" + val userLink = "https://matrix.to/#/${userId.value}" + val cleanOriginalMessage = originalMessage.replace(MX_REPLY_REGEX, "") + return """ +
In reply to ${userId.value}
${cleanOriginalMessage}
$reply + """.trimIndent() + + } + } diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt index 2057084..1d64872 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt @@ -1,7 +1,6 @@ package app.dapk.st.matrix.message.internal -import app.dapk.st.matrix.common.EventType -import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.common.* import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest import app.dapk.st.matrix.http.jsonBody @@ -14,6 +13,8 @@ import app.dapk.st.matrix.message.internal.ApiMessage.TextMessage import io.ktor.http.* import io.ktor.http.content.* import io.ktor.utils.io.jvm.javaio.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import java.io.InputStream import java.util.* @@ -26,10 +27,29 @@ internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, con } ) -internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, content: MessageEncrypter.EncryptedMessagePayload) = httpRequest( +internal fun sendRequest( + roomId: RoomId, + eventType: EventType, + txId: String, + content: MessageEncrypter.EncryptedMessagePayload, + relatesTo: ApiMessage.RelatesTo? +) = httpRequest( path = "_matrix/client/r0/rooms/${roomId.value}/send/${eventType.value}/${txId}", method = MatrixHttpClient.Method.PUT, - body = jsonBody(MessageEncrypter.EncryptedMessagePayload.serializer(), content) + body = jsonBody(ApiEncryptedMessage.serializer(), content.let { + val apiEncryptedMessage = ApiEncryptedMessage( + algorithmName = content.algorithmName, + senderKey = content.senderKey, + cipherText = content.cipherText, + sessionId = content.sessionId, + deviceId = content.deviceId, + ) + when (relatesTo) { + null -> apiEncryptedMessage + else -> apiEncryptedMessage.copy(relatesTo = relatesTo) + } + + }) ) internal fun sendRequest(roomId: RoomId, eventType: EventType, content: EventMessage) = httpRequest( @@ -51,4 +71,14 @@ internal fun uploadRequest(stream: InputStream, contentLength: Long, filename: S ), ) -fun txId() = "local.${UUID.randomUUID()}" \ No newline at end of file +fun txId() = "local.${UUID.randomUUID()}" + +@Serializable +data class ApiEncryptedMessage( + @SerialName("algorithm") val algorithmName: AlgorithmName, + @SerialName("sender_key") val senderKey: String, + @SerialName("ciphertext") val cipherText: CipherText, + @SerialName("session_id") val sessionId: SessionId, + @SerialName("device_id") val deviceId: DeviceId, + @SerialName("m.relates_to") val relatesTo: ApiMessage.RelatesTo? = null, +) \ No newline at end of file From 1c0f51cfb738ab1f2d9bee8528a3768d5915fa2f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 1 Oct 2022 10:50:07 +0100 Subject: [PATCH 12/38] fixing test compilation error --- .../app/dapk/st/messenger/MessengerViewModelTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt index bca16fe..d05bee3 100644 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt +++ b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt @@ -59,7 +59,7 @@ class MessengerViewModelTest { MessengerScreenState( roomId = null, roomState = Lce.Loading(), - composerState = ComposerState.Text(value = "") + composerState = ComposerState.Text(value = "", reply = null) ) ) } @@ -85,7 +85,7 @@ class MessengerViewModelTest { viewModel.test().post(MessengerAction.ComposerTextUpdate(A_MESSAGE_CONTENT)) assertStates({ - copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT)) + copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = null)) }) } @@ -95,7 +95,7 @@ class MessengerViewModelTest { viewModel.test(initialState = initialStateWithComposerMessage(A_ROOM_ID, A_MESSAGE_CONTENT)).post(MessengerAction.ComposerSendText) - assertStates({ copy(composerState = ComposerState.Text("")) }) + assertStates({ copy(composerState = ComposerState.Text("", reply = null)) }) verifyExpects() } @@ -126,7 +126,7 @@ class MessengerViewModelTest { fun aMessageScreenState(roomId: RoomId = aRoomId(), roomState: MessengerState, messageContent: String?) = MessengerScreenState( roomId = roomId, roomState = Lce.Content(roomState), - composerState = ComposerState.Text(value = messageContent ?: "") + composerState = ComposerState.Text(value = messageContent ?: "", reply = null) ) fun aMessengerState( From 955c6152307dbe4dec09142a094569964861f59f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 1 Oct 2022 10:52:03 +0100 Subject: [PATCH 13/38] removing unused field --- .../src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt index 2c2ff69..ea852e2 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt @@ -40,7 +40,6 @@ interface MessageService : MatrixService { @SerialName("local_id") val localId: String, @SerialName("timestamp") val timestampUtc: Long, @SerialName("reply") val reply: Reply? = null, - @SerialName("reply_id") val replyId: String? = null, ) : Message() { @Serializable data class Reply( From 2162e85496fe564480f75db4e184076422b59560 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 1 Oct 2022 10:54:23 +0100 Subject: [PATCH 14/38] removing redundant serializable --- .../app/dapk/st/matrix/message/MessageEncrypter.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageEncrypter.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageEncrypter.kt index d2387e8..a9d06a9 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageEncrypter.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageEncrypter.kt @@ -1,20 +1,17 @@ package app.dapk.st.matrix.message import app.dapk.st.matrix.common.* -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable fun interface MessageEncrypter { suspend fun encrypt(message: ClearMessagePayload): EncryptedMessagePayload - @Serializable data class EncryptedMessagePayload( - @SerialName("algorithm") val algorithmName: AlgorithmName, - @SerialName("sender_key") val senderKey: String, - @SerialName("ciphertext") val cipherText: CipherText, - @SerialName("session_id") val sessionId: SessionId, - @SerialName("device_id") val deviceId: DeviceId + val algorithmName: AlgorithmName, + val senderKey: String, + val cipherText: CipherText, + val sessionId: SessionId, + val deviceId: DeviceId ) data class ClearMessagePayload( From d67d1eda13874a09b1cda61669808b830f32f808 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 1 Oct 2022 11:28:06 +0100 Subject: [PATCH 15/38] replacing sealed class with interface --- .../kotlin/app/dapk/st/matrix/message/MessageService.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt index ea852e2..e488b04 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt @@ -30,7 +30,7 @@ interface MessageService : MatrixService { } @Serializable - sealed class Message { + sealed interface Message { @Serializable @SerialName("text_message") data class TextMessage( @@ -40,7 +40,7 @@ interface MessageService : MatrixService { @SerialName("local_id") val localId: String, @SerialName("timestamp") val timestampUtc: Long, @SerialName("reply") val reply: Reply? = null, - ) : Message() { + ) : Message { @Serializable data class Reply( val authorId: UserId, @@ -58,7 +58,7 @@ interface MessageService : MatrixService { @SerialName("room_id") val roomId: RoomId, @SerialName("local_id") val localId: String, @SerialName("timestamp") val timestampUtc: Long, - ) : Message() + ) : Message @Serializable sealed class Content { From ae693f7ee8e8952491ea1117fb9e6026781ce5c6 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 1 Oct 2022 11:47:42 +0100 Subject: [PATCH 16/38] supporting local echos for text replies --- .../app/dapk/st/messenger/LocalEchoMapper.kt | 16 +++++++++++++++- .../app/dapk/st/messenger/MessengerViewModel.kt | 7 ++++--- .../app/dapk/st/matrix/message/MessageService.kt | 5 +++-- .../message/internal/SendMessageUseCase.kt | 4 ++-- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt index a27894f..e65bb0d 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt @@ -11,14 +11,28 @@ internal class LocalEchoMapper(private val metaMapper: MetaMapper) { fun MessageService.LocalEcho.toMessage(member: RoomMember): RoomEvent { return when (val message = this.message) { is MessageService.Message.TextMessage -> { - RoomEvent.Message( + val mappedMessage = RoomEvent.Message( eventId = this.eventId ?: EventId(this.localId), content = message.content.body, author = member, utcTimestamp = message.timestampUtc, meta = metaMapper.toMeta(this) ) + + when (val reply = message.reply) { + null -> mappedMessage + else -> RoomEvent.Reply( + mappedMessage, RoomEvent.Message( + eventId = reply.eventId, + content = reply.originalMessage, + author = reply.author, + utcTimestamp = reply.timestampUtc, + meta = MessageMeta.FromServer + ) + ) + } } + is MessageService.Message.ImageMessage -> { RoomEvent.Image( eventId = this.eventId ?: EventId(this.localId), diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt index a31b1b0..e01b55a 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt @@ -123,14 +123,15 @@ internal class MessengerViewModel( timestampUtc = clock.millis(), reply = copy.reply?.let { MessageService.Message.TextMessage.Reply( - authorId = it.author.id, + author = it.author, originalMessage = when (it) { is RoomEvent.Image -> TODO() is RoomEvent.Reply -> TODO() is RoomEvent.Message -> it.content }, - copy.value, - it.eventId + replyContent = copy.value, + eventId = it.eventId, + timestampUtc = it.utcTimestamp, ) } ) diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt index e488b04..475ae1f 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt @@ -43,10 +43,11 @@ interface MessageService : MatrixService { ) : Message { @Serializable data class Reply( - val authorId: UserId, + val author: RoomMember, val originalMessage: String, val replyContent: String, - val eventId: EventId + val eventId: EventId, + val timestampUtc: Long, ) } diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt index aa573f2..385fffb 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt @@ -157,9 +157,9 @@ class ApiMessageMapper { ) else -> ApiMessage.TextMessage.TextContent( - body = buildReplyFallback(reply.originalMessage, reply.authorId, reply.replyContent), + body = buildReplyFallback(reply.originalMessage, reply.author.id, reply.replyContent), relatesTo = ApiMessage.RelatesTo(ApiMessage.RelatesTo.InReplyTo(reply.eventId)), - formattedBody = buildFormattedReply(reply.authorId, reply.originalMessage, reply.replyContent, this.roomId, reply.eventId), + formattedBody = buildFormattedReply(reply.author.id, reply.originalMessage, reply.replyContent, this.roomId, reply.eventId), format = "org.matrix.custom.html" ) } From 2f9bcb8ccf939ccb33570c12566fb6b6645eacf2 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 14:14:22 +0100 Subject: [PATCH 17/38] allowing the canonical alias to be null when parsing the sync response --- .../dapk/st/matrix/sync/internal/request/ApiTimelineEvent.kt | 2 +- .../dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiTimelineEvent.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiTimelineEvent.kt index f4ecf19..0081e26 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiTimelineEvent.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiTimelineEvent.kt @@ -65,7 +65,7 @@ internal sealed class ApiTimelineEvent { @Serializable internal data class Content( - @SerialName("alias") val alias: String + @SerialName("alias") val alias: String? = null ) } diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt index d4d4ebf..f4a9149 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt @@ -66,7 +66,7 @@ internal class RoomOverviewProcessor( private suspend fun roomDisplayName(roomToProcess: RoomToProcess, combinedEvents: List): String? { val roomName = combinedEvents.filterIsInstance().lastOrNull()?.content?.name - ?: combinedEvents.filterIsInstance().lastOrNull()?.content?.alias + ?: combinedEvents.filterIsInstance().lastOrNull()?.content?.alias?.takeIf { it.isNotEmpty() } ?: roomToProcess.heroes?.let { roomMembersService.find(roomToProcess.roomId, it).joinToString { it.displayName ?: it.id.value } } From 8d7a15e7587a4c8f7bbc4ac805b574b3c654f4cb Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 14:47:22 +0100 Subject: [PATCH 18/38] adding button to dismiss inprogress reply --- .../app/dapk/st/messenger/MessengerScreen.kt | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) 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 3fa4259..1de7cb4 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 @@ -28,7 +28,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.onSizeChanged @@ -83,6 +82,11 @@ internal fun MessengerScreen( else -> null } + val replyActions = ReplyActions( + onReply = { viewModel.post(MessengerAction.ComposerEnterReplyMode(it)) }, + onDismiss = { viewModel.post(MessengerAction.ComposerExitReplyMode) } + ) + Column { Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = { // OverflowMenu { @@ -91,14 +95,13 @@ internal fun MessengerScreen( }) when (state.composerState) { is ComposerState.Text -> { - Room(state.roomState, onReply = { - viewModel.post(MessengerAction.ComposerEnterReplyMode(it)) - }) + Room(state.roomState, replyActions) TextComposer( state.composerState, onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) }, onSend = { viewModel.post(MessengerAction.ComposerSendText) }, - onAttach = { viewModel.startAttachment() } + onAttach = { viewModel.startAttachment() }, + replyActions = replyActions, ) } @@ -129,11 +132,11 @@ private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLaun } @Composable -private fun ColumnScope.Room(roomStateLce: Lce, onReply: (RoomEvent) -> Unit) { +private fun ColumnScope.Room(roomStateLce: Lce, replyActions: ReplyActions) { when (val state = roomStateLce) { is Lce.Loading -> CenteredLoading() is Lce.Content -> { - RoomContent(state.value.self, state.value.roomState, onReply) + RoomContent(state.value.self, state.value.roomState, replyActions) val eventBarHeight = 14.dp val typing = state.value.typing when { @@ -176,7 +179,7 @@ private fun ColumnScope.Room(roomStateLce: Lce, onReply: (RoomEv } @Composable -private fun ColumnScope.RoomContent(self: UserId, state: RoomState, onReply: (RoomEvent) -> Unit) { +private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions: ReplyActions) { val listState: LazyListState = rememberLazyListState( initialFirstVisibleItemIndex = 0 ) @@ -202,7 +205,7 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, onReply: (Ro ) { index, item -> val previousEvent = if (index != 0) state.events[index - 1] else null val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id - AlignedBubble(item, self, wasPreviousMessageSameSender, onReply) { + AlignedBubble(item, self, wasPreviousMessageSameSender, replyActions) { when (item) { is RoomEvent.Image -> MessageImage(it as BubbleContent) is Message -> TextBubbleContent(it as BubbleContent) @@ -225,7 +228,7 @@ private fun LazyItemScope.AlignedBubble( message: T, self: UserId, wasPreviousMessageSameSender: Boolean, - onReply: (RoomEvent) -> Unit, + replyActions: ReplyActions, content: @Composable (BubbleContent) -> Unit ) { when (message.author.id == self) { @@ -236,7 +239,7 @@ private fun LazyItemScope.AlignedBubble( message = message, isNotSelf = false, wasPreviousMessageSameSender = wasPreviousMessageSameSender, - onReply = onReply, + replyActions = replyActions, ) { content(BubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message)) } @@ -250,7 +253,7 @@ private fun LazyItemScope.AlignedBubble( message = message, isNotSelf = true, wasPreviousMessageSameSender = wasPreviousMessageSameSender, - onReply = onReply, + replyActions = replyActions, ) { content(BubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message)) } @@ -345,7 +348,7 @@ private fun Bubble( message: RoomEvent, isNotSelf: Boolean, wasPreviousMessageSameSender: Boolean, - onReply: (RoomEvent) -> Unit, + replyActions: ReplyActions, content: @Composable () -> Unit ) { @@ -368,7 +371,7 @@ private fun Bubble( onDragStopped = { with(localDensity) { if (offsetX.value > (screenWidthDp.toPx() * 0.15)) { - onReply(message) + replyActions.onReply(message) } } @@ -619,7 +622,7 @@ private fun RowScope.SendStatus(message: RoomEvent) { @OptIn(ExperimentalAnimationApi::class) @Composable -private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Unit, onSend: () -> Unit, onAttach: () -> Unit) { +private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Unit, onSend: () -> Unit, onAttach: () -> Unit, replyActions: ReplyActions) { Row( Modifier .fillMaxWidth() @@ -636,22 +639,25 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un AnimatedContent( targetState = state.reply, transitionSpec = { - slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Up, animationSpec = tween(500)) { - it / 2 - } - .with(slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Down)) + val durationMillis = 300 + slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Up, tween(durationMillis)) { it / 2 } + .with(slideOutVertically(tween(durationMillis)) { it / 2}) .using( SizeTransform( clip = true, - sizeAnimationSpec = { initialSize, targetSize -> - tween(500) - }) - + sizeAnimationSpec = { _, _ -> tween(durationMillis) }) ) } ) { if (it is Message) { - Column(Modifier.padding(12.dp)) { + Box(Modifier.padding(12.dp)) { + Box(Modifier.padding(4.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) { + Icon( + modifier = Modifier.size(16.dp), + imageVector = Icons.Filled.Close, + contentDescription = "", + ) + } Column( Modifier .fillMaxWidth() @@ -776,3 +782,7 @@ private fun AttachmentComposer(state: ComposerState.Attachments, onSend: () -> U } } +class ReplyActions( + val onReply: (RoomEvent) -> Unit, + val onDismiss: () -> Unit, +) From 7f9f761eabd4c298e35b64eb56464fae214fbcc6 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 15:42:26 +0100 Subject: [PATCH 19/38] using bubble colour with alpha for the reply background + some tweaks to padding and corners --- .../main/kotlin/app/dapk/st/messenger/MessengerScreen.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 1de7cb4..3d71f5d 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 @@ -477,8 +477,8 @@ private fun ReplyBubbleContent(content: BubbleContent) { Column( Modifier .fillMaxWidth() - .background(if (content.isNotSelf) SmallTalkTheme.extendedColors.otherBubbleReplyBackground else SmallTalkTheme.extendedColors.selfBubbleReplyBackground) - .padding(4.dp) + .background(if (content.isNotSelf) SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.1f) else SmallTalkTheme.extendedColors.onSelfBubble.copy(alpha = 0.2f), RoundedCornerShape(12.dp)) + .padding(8.dp) ) { val replyName = if (!content.isNotSelf && content.message.replyingToSelf) "You" else content.message.replyingTo.author.displayName ?: content.message.replyingTo.author.id.value @@ -488,12 +488,13 @@ private fun ReplyBubbleContent(content: BubbleContent) { maxLines = 1, color = content.textColor() ) + Spacer(modifier = Modifier.height(2.dp)) when (val replyingTo = content.message.replyingTo) { is Message -> { Text( text = replyingTo.content, - color = content.textColor(), - fontSize = 15.sp, + color = content.textColor().copy(alpha = 0.8f), + fontSize = 14.sp, modifier = Modifier.wrapContentSize(), textAlign = TextAlign.Start, ) From 906ec486f8c1d9e1bce42cadd768fca55ee74480 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 15:54:30 +0100 Subject: [PATCH 20/38] styling the inline composer rely to match the reply messages --- .../app/dapk/st/messenger/MessengerScreen.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 3d71f5d..c06f67c 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 @@ -477,7 +477,11 @@ private fun ReplyBubbleContent(content: BubbleContent) { Column( Modifier .fillMaxWidth() - .background(if (content.isNotSelf) SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.1f) else SmallTalkTheme.extendedColors.onSelfBubble.copy(alpha = 0.2f), RoundedCornerShape(12.dp)) + .background( + if (content.isNotSelf) SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.1f) else SmallTalkTheme.extendedColors.onSelfBubble.copy( + alpha = 0.2f + ), RoundedCornerShape(12.dp) + ) .padding(8.dp) ) { val replyName = if (!content.isNotSelf && content.message.replyingToSelf) "You" else content.message.replyingTo.author.displayName @@ -642,7 +646,7 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un transitionSpec = { val durationMillis = 300 slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Up, tween(durationMillis)) { it / 2 } - .with(slideOutVertically(tween(durationMillis)) { it / 2}) + .with(slideOutVertically(tween(durationMillis)) { it / 2 }) .using( SizeTransform( clip = true, @@ -652,7 +656,7 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un ) { if (it is Message) { Box(Modifier.padding(12.dp)) { - Box(Modifier.padding(4.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) { + Box(Modifier.padding(8.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) { Icon( modifier = Modifier.size(16.dp), imageVector = Icons.Filled.Close, @@ -662,8 +666,8 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un Column( Modifier .fillMaxWidth() - .background(SmallTalkTheme.extendedColors.selfBubbleReplyBackground) - .padding(4.dp) + .background(SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.1f), RoundedCornerShape(12.dp)) + .padding(8.dp) ) { val replyName = it.author.displayName ?: it.author.id.value Text( @@ -676,7 +680,7 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un Text( text = it.content, color = SmallTalkTheme.extendedColors.onSelfBubble, - fontSize = 15.sp, + fontSize = 14.sp, maxLines = 2, modifier = Modifier.wrapContentSize(), textAlign = TextAlign.Start, From 2bb2091234ba6315a2d0466c6ff3c3c696c02d44 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 16:26:44 +0100 Subject: [PATCH 21/38] switching to lazy variant configuration --- app/build.gradle | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0c81f66..a52087c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,11 +16,6 @@ android { if (isDebugBuild) { resConfigs "en", "xxhdpi" - variantFilter { variant -> - if (variant.buildType.name == "release") { - setIgnore(true) - } - } } else { resConfigs "en" } @@ -67,6 +62,13 @@ android { } } +if (isDebugBuild) { + androidComponents { + def release = selector().withBuildType("release") + beforeVariants(release) { it.enabled = false } + } +} + dependencies { coreLibraryDesugaring Dependencies.google.jdkLibs From 2110667d90dfb1198fc59fc316ba0aa057ed37c6 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 1 Oct 2022 12:22:19 +0100 Subject: [PATCH 22/38] computing the image size before sending to allow it to be used in local echos - fixes local echo images changing size --- .../main/kotlin/app/dapk/st/graph/AppModule.kt | 10 +++++++--- .../app/dapk/st/messenger/LocalEchoMapper.kt | 2 +- .../app/dapk/st/messenger/MessengerModule.kt | 4 +++- .../dapk/st/messenger/MessengerViewModel.kt | 15 ++++++++++++++- .../st/messenger/MessengerViewModelTest.kt | 4 ++++ .../dapk/st/matrix/message/MessageService.kt | 18 +++++++++++++++--- .../message/internal/SendMessageUseCase.kt | 2 +- test-harness/src/test/kotlin/test/Test.kt | 2 +- 8 files changed, 46 insertions(+), 11 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 f424c2b..0732e31 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -93,7 +93,9 @@ internal class AppModule(context: Application, logger: MatrixLogger) { private val workModule = WorkModule(context) private val imageLoaderModule = ImageLoaderModule(context) - private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, context.contentResolver, base64, buildMeta) + private val imageContentReader by unsafeLazy { AndroidImageContentReader(context.contentResolver) } + private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, imageContentReader, base64, buildMeta) + val domainModules = DomainModules(matrixModules, trackingModule.errorTracker, workModule, storeModule, context, coroutineDispatchers) val coreAndroidModule = CoreAndroidModule( @@ -133,6 +135,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) { trackingModule, coreAndroidModule, imageLoaderModule, + imageContentReader, context, buildMeta, deviceMeta, @@ -149,6 +152,7 @@ internal class FeatureModules internal constructor( private val trackingModule: TrackingModule, private val coreAndroidModule: CoreAndroidModule, imageLoaderModule: ImageLoaderModule, + imageContentReader: ImageContentReader, context: Context, buildMeta: BuildMeta, deviceMeta: DeviceMeta, @@ -185,6 +189,7 @@ internal class FeatureModules internal constructor( clock, context, base64, + imageContentReader, ) } val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile, matrixModules.sync, buildMeta) } @@ -238,7 +243,7 @@ internal class MatrixModules( private val workModule: WorkModule, private val logger: MatrixLogger, private val coroutineDispatchers: CoroutineDispatchers, - private val contentResolver: ContentResolver, + private val imageContentReader: ImageContentReader, private val base64: Base64, private val buildMeta: BuildMeta, ) { @@ -280,7 +285,6 @@ internal class MatrixModules( base64 = base64, coroutineDispatchers = coroutineDispatchers, ) - val imageContentReader = AndroidImageContentReader(contentResolver) installMessageService( store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler()), diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt index e65bb0d..8fc2fa7 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt @@ -39,7 +39,7 @@ internal class LocalEchoMapper(private val metaMapper: MetaMapper) { author = member, utcTimestamp = message.timestampUtc, meta = metaMapper.toMeta(this), - imageMeta = RoomEvent.Image.ImageMeta(100, 100, message.content.uri, null), + imageMeta = RoomEvent.Image.ImageMeta(message.content.meta.width, message.content.meta.height, message.content.uri, null), ) } } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt index 90446f9..4caff56 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt @@ -6,6 +6,7 @@ import app.dapk.st.core.ProvidableModule import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.message.MessageService +import app.dapk.st.matrix.message.internal.ImageContentReader import app.dapk.st.matrix.room.RoomService import app.dapk.st.matrix.sync.RoomStore import app.dapk.st.matrix.sync.SyncService @@ -20,10 +21,11 @@ class MessengerModule( private val clock: Clock, private val context: Context, private val base64: Base64, + private val imageMetaReader: ImageContentReader, ) : ProvidableModule { internal fun messengerViewModel(): MessengerViewModel { - return MessengerViewModel(messageService, roomService, roomStore, credentialsStore, timelineUseCase(), LocalIdFactory(), clock) + return MessengerViewModel(messageService, roomService, roomStore, credentialsStore, timelineUseCase(), LocalIdFactory(), imageMetaReader, clock) } private fun timelineUseCase(): TimelineUseCaseImpl { diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt index e01b55a..4ba6163 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt @@ -8,6 +8,7 @@ import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.message.MessageService +import app.dapk.st.matrix.message.internal.ImageContentReader import app.dapk.st.matrix.room.RoomService import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomStore @@ -28,6 +29,7 @@ internal class MessengerViewModel( private val credentialsStore: CredentialsStore, private val observeTimeline: ObserveTimelineUseCase, private val localIdFactory: LocalIdFactory, + private val imageContentReader: ImageContentReader, private val clock: Clock, factory: MutableStateFactory = defaultStateFactory(), ) : DapkViewModel( @@ -147,9 +149,20 @@ internal class MessengerViewModel( state.roomState.takeIfContent()?.let { content -> val roomState = content.roomState viewModelScope.launch { + val imageUri = copy.values.first().uri.value + val meta = imageContentReader.meta(imageUri) + messageService.scheduleMessage( MessageService.Message.ImageMessage( - MessageService.Message.Content.ApiImageContent(uri = copy.values.first().uri.value), + MessageService.Message.Content.ImageContent( + uri = imageUri, MessageService.Message.Content.ImageContent.Meta( + height = meta.height, + width = meta.width, + size = meta.size, + fileName = meta.fileName, + mimeType = meta.mimeType, + ) + ), roomId = roomState.roomOverview.roomId, sendEncrypted = roomState.roomOverview.isEncrypted, localId = localIdFactory.create(), diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt index d05bee3..2ce7567 100644 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt +++ b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt @@ -6,6 +6,7 @@ import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.message.MessageService +import app.dapk.st.matrix.message.internal.ImageContentReader import app.dapk.st.matrix.room.RoomService import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.SyncService @@ -47,6 +48,7 @@ class MessengerViewModelTest { fakeCredentialsStore, fakeObserveTimelineUseCase, localIdFactory = FakeLocalIdFactory().also { it.givenCreate().returns(A_LOCAL_ID) }.instance, + imageContentReader = FakeImageContentReader(), clock = fixedClock(A_CURRENT_TIMESTAMP), factory = runViewModelTest.testMutableStateFactory(), ) @@ -150,3 +152,5 @@ class FakeRoomService : RoomService by mockk() { } fun fixedClock(timestamp: Long = 0) = Clock.fixed(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC) + +class FakeImageContentReader: ImageContentReader by mockk() \ No newline at end of file diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt index 475ae1f..94ee5a9 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt @@ -54,7 +54,7 @@ interface MessageService : MatrixService { @Serializable @SerialName("image_message") data class ImageMessage( - @SerialName("content") val content: Content.ApiImageContent, + @SerialName("content") val content: Content.ImageContent, @SerialName("send_encrypted") val sendEncrypted: Boolean, @SerialName("room_id") val roomId: RoomId, @SerialName("local_id") val localId: String, @@ -70,9 +70,21 @@ interface MessageService : MatrixService { ) : Content() @Serializable - data class ApiImageContent( + data class ImageContent( @SerialName("uri") val uri: String, - ) : Content() + @SerialName("meta") val meta: Meta, + ) : Content() { + + @Serializable + data class Meta( + val height: Int, + val width: Int, + val size: Long, + val fileName: String, + val mimeType: String, + ) + + } } } diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt index 385fffb..0703d8d 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt @@ -59,7 +59,7 @@ internal class SendMessageUseCase( } private suspend fun imageMessageRequest(message: Message.ImageMessage): HttpRequest { - val imageMeta = imageContentReader.meta(message.content.uri) + val imageMeta = message.content.meta return when (message.sendEncrypted) { true -> { diff --git a/test-harness/src/test/kotlin/test/Test.kt b/test-harness/src/test/kotlin/test/Test.kt index 08b4e4c..222b06e 100644 --- a/test-harness/src/test/kotlin/test/Test.kt +++ b/test-harness/src/test/kotlin/test/Test.kt @@ -183,7 +183,7 @@ class MatrixTestScope(private val testScope: TestScope) { println("sending ${file.name}") this.client.messageService().scheduleMessage( MessageService.Message.ImageMessage( - content = MessageService.Message.Content.ApiImageContent( + content = MessageService.Message.Content.ImageContent( uri = file.absolutePath, ), roomId = roomId, From 55cdedd5af7e1764afa73a18948c7a86de0876a6 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 1 Oct 2022 13:07:37 +0100 Subject: [PATCH 23/38] avoiding sync event updates from other rooms --- .../src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt | 6 ++++-- .../app/dapk/st/matrix/sync/internal/DefaultSyncService.kt | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt index d0487d3..20ddf80 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt @@ -19,7 +19,7 @@ interface SyncService : MatrixService { fun overview(): Flow fun room(roomId: RoomId): Flow fun startSyncing(): Flow - fun events(): Flow> + fun events(roomId: RoomId? = null): Flow> suspend fun observeEvent(eventId: EventId): Flow suspend fun forceManualRefresh(roomIds: List) @@ -27,7 +27,9 @@ interface SyncService : MatrixService { value class FilterId(val value: String) sealed interface SyncEvent { - data class Typing(val roomId: RoomId, val members: List) : SyncEvent + val roomId: RoomId + + data class Typing(override val roomId: RoomId, val members: List) : SyncEvent } } diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt index bf8a7ae..779c2f0 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt @@ -108,7 +108,7 @@ internal class DefaultSyncService( override fun invites() = overviewStore.latestInvites() override fun overview() = overviewStore.latest() override fun room(roomId: RoomId) = roomStore.latest(roomId) - override fun events() = syncEventsFlow + override fun events(roomId: RoomId?) = roomId?.let { syncEventsFlow.map { it.filter { it.roomId == roomId } }.distinctUntilChanged() } ?: syncEventsFlow override suspend fun observeEvent(eventId: EventId) = roomStore.observeEvent(eventId) override suspend fun forceManualRefresh(roomIds: List) { coroutineDispatchers.withIoContext { From 78c62ab7bb2a9b9c4c2ca76aa4933c3569e8009b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 1 Oct 2022 14:03:30 +0100 Subject: [PATCH 24/38] avoiding duplicated room event emissions when watching a specific room id --- .../kotlin/app/dapk/st/domain/sync/RoomPersistence.kt | 8 +++----- .../main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt | 9 +++------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt index 7523e1d..5adf63e 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt @@ -13,10 +13,7 @@ import app.dapk.st.matrix.sync.RoomStore import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.mapToList import com.squareup.sqldelight.runtime.coroutines.mapToOneNotNull -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* import kotlinx.serialization.json.Json private val json = Json @@ -54,12 +51,13 @@ internal class RoomPersistence( override fun latest(roomId: RoomId): Flow { val overviewFlow = database.overviewStateQueries.selectRoom(roomId.value).asFlow().mapToOneNotNull().map { json.decodeFromString(RoomOverview.serializer(), it) - } + }.distinctUntilChanged() return database.roomEventQueries.selectRoom(roomId.value) .asFlow() .mapToList() .map { it.map { json.decodeFromString(RoomEvent.serializer(), it) } } + .distinctUntilChanged() .combine(overviewFlow) { events, overview -> RoomState(overview, events) } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt index c117f39..629c466 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt @@ -7,10 +7,7 @@ import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.room.RoomService import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.SyncService -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.* internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow @@ -25,7 +22,7 @@ internal class TimelineUseCaseImpl( return combine( roomDatasource(roomId), messageService.localEchos(roomId), - syncService.events() + syncService.events(roomId) ) { roomState, localEchos, events -> MessengerState( roomState = when { @@ -45,7 +42,7 @@ internal class TimelineUseCaseImpl( } private fun roomDatasource(roomId: RoomId) = combine( - syncService.startSyncing().map { false }.onStart { emit(true) }, + syncService.startSyncing().map { false }.onStart { emit(true) }.filter { it }, syncService.room(roomId) ) { _, room -> room } } From 0450dc869d3a4c13854df480a40c3088183177b5 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 16:06:42 +0100 Subject: [PATCH 25/38] extracting flow extension for starting and ignoring future emissions --- .../app/dapk/st/core/extensions/FlowExtensions.kt | 10 ++++------ .../kotlin/app/dapk/st/messenger/TimelineUseCase.kt | 6 ++++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/app/dapk/st/core/extensions/FlowExtensions.kt b/core/src/main/kotlin/app/dapk/st/core/extensions/FlowExtensions.kt index 2ffc7da..92be2fb 100644 --- a/core/src/main/kotlin/app/dapk/st/core/extensions/FlowExtensions.kt +++ b/core/src/main/kotlin/app/dapk/st/core/extensions/FlowExtensions.kt @@ -1,11 +1,7 @@ package app.dapk.st.core.extensions -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.flow.* -@OptIn(InternalCoroutinesApi::class) suspend fun Flow.firstOrNull(count: Int, predicate: suspend (T) -> Boolean): T? { var counter = 0 @@ -21,4 +17,6 @@ suspend fun Flow.firstOrNull(count: Int, predicate: suspend (T) -> Boolea } return result -} \ No newline at end of file +} + +fun Flow.startAndIgnoreEmissions(): Flow = this.map { false }.onStart { emit(true) }.filter { it } \ No newline at end of file diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt index 629c466..c1d9538 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt @@ -1,5 +1,6 @@ package app.dapk.st.messenger +import app.dapk.st.core.extensions.startAndIgnoreEmissions import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.UserId @@ -7,7 +8,8 @@ import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.room.RoomService import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.SyncService -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow @@ -42,7 +44,7 @@ internal class TimelineUseCaseImpl( } private fun roomDatasource(roomId: RoomId) = combine( - syncService.startSyncing().map { false }.onStart { emit(true) }.filter { it }, + syncService.startSyncing().startAndIgnoreEmissions(), syncService.room(roomId) ) { _, room -> room } } From abcf3193a8c0c608810d7001ba2ebdee236e5d74 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 16:16:00 +0100 Subject: [PATCH 26/38] fixing test harness compilation error --- test-harness/src/test/kotlin/test/Test.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test-harness/src/test/kotlin/test/Test.kt b/test-harness/src/test/kotlin/test/Test.kt index 222b06e..7ff5604 100644 --- a/test-harness/src/test/kotlin/test/Test.kt +++ b/test-harness/src/test/kotlin/test/Test.kt @@ -185,6 +185,15 @@ class MatrixTestScope(private val testScope: TestScope) { MessageService.Message.ImageMessage( content = MessageService.Message.Content.ImageContent( uri = file.absolutePath, + meta = JavaImageContentReader().meta(file.absolutePath).let { + MessageService.Message.Content.ImageContent.Meta( + height = it.height, + width = it.width, + size = it.size, + fileName = it.fileName, + mimeType = it.mimeType, + ) + } ), roomId = roomId, sendEncrypted = isEncrypted, From 3f1fec1d8d52ef72e71192af5c3e55b896199425 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 16:29:06 +0100 Subject: [PATCH 27/38] updating sync service fake to reflect events by roomid change --- .../sync/src/testFixtures/kotlin/fake/FakeSyncService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeSyncService.kt b/matrix/services/sync/src/testFixtures/kotlin/fake/FakeSyncService.kt index f264259..3909f95 100644 --- a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeSyncService.kt +++ b/matrix/services/sync/src/testFixtures/kotlin/fake/FakeSyncService.kt @@ -14,6 +14,6 @@ class FakeSyncService : SyncService by mockk() { fun givenRoom(roomId: RoomId) = every { room(roomId) }.delegateReturn() - fun givenEvents(roomId: RoomId) = every { events() }.delegateReturn() + fun givenEvents(roomId: RoomId) = every { events(roomId) }.delegateReturn() } From 86d41fd95f474c545b25f49d2d0612215723ab14 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 16:58:59 +0100 Subject: [PATCH 28/38] providing a custom initial device display name - Uses SmallTalk Android with a 4 character random --- app/src/main/kotlin/app/dapk/st/graph/AppModule.kt | 11 ++++++++++- .../kotlin/app/dapk/st/matrix/auth/AuthService.kt | 11 ++++++----- .../app/dapk/st/matrix/auth/internal/AuthRequest.kt | 5 +++-- .../st/matrix/auth/internal/DefaultAuthService.kt | 6 ++++-- .../internal/LoginWithUserPasswordServerUseCase.kt | 6 ++++-- .../auth/internal/LoginWithUserPasswordUseCase.kt | 5 ++++- 6 files changed, 31 insertions(+), 13 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 0732e31..d51c69d 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -23,6 +23,7 @@ import app.dapk.st.home.MainActivity import app.dapk.st.imageloader.ImageLoaderModule import app.dapk.st.login.LoginModule import app.dapk.st.matrix.MatrixClient +import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator import app.dapk.st.matrix.auth.authService import app.dapk.st.matrix.auth.installAuthService import app.dapk.st.matrix.common.* @@ -259,7 +260,7 @@ internal class MatrixModules( logger ).also { it.install { - installAuthService(credentialsStore) + installAuthService(credentialsStore, SmallTalkDeviceNameGenerator()) installEncryptionService(store.knownDevicesStore()) val olmAccountStore = OlmPersistenceWrapper(store.olmStore(), base64) @@ -516,4 +517,12 @@ internal class AndroidImageContentReader(private val contentResolver: ContentRes } override fun inputStream(uri: String): InputStream = contentResolver.openInputStream(Uri.parse(uri))!! +} + +internal class SmallTalkDeviceNameGenerator : DeviceDisplayNameGenerator { + override fun generate(): String { + val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') + val randomIdentifier = (1..4).map { allowedChars.random() }.joinToString("") + return "SmallTalk Android ($randomIdentifier)" + } } \ No newline at end of file diff --git a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/AuthService.kt b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/AuthService.kt index 348df96..b9639b3 100644 --- a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/AuthService.kt +++ b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/AuthService.kt @@ -26,16 +26,17 @@ interface AuthService : MatrixService { fun MatrixServiceInstaller.installAuthService( credentialsStore: CredentialsStore, + deviceDisplayNameGenerator: DeviceDisplayNameGenerator = DefaultDeviceDisplayNameGenerator, ): InstallExtender { return this.install { (httpClient, json) -> - SERVICE_KEY to DefaultAuthService(httpClient, credentialsStore, json) + SERVICE_KEY to DefaultAuthService(httpClient, credentialsStore, json, deviceDisplayNameGenerator) } } fun MatrixClient.authService(): AuthService = this.getService(key = SERVICE_KEY) +fun interface DeviceDisplayNameGenerator { + fun generate(): String? +} -data class AuthConfig( - val forceHttp: Boolean = false, - val forceHomeserver: String? = null -) +val DefaultDeviceDisplayNameGenerator = DeviceDisplayNameGenerator { null } \ No newline at end of file diff --git a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/AuthRequest.kt b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/AuthRequest.kt index ef48a15..94264da 100644 --- a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/AuthRequest.kt +++ b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/AuthRequest.kt @@ -11,12 +11,12 @@ import app.dapk.st.matrix.http.jsonBody import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -fun loginRequest(userId: UserId, password: String, baseUrl: String) = httpRequest( +fun loginRequest(userId: UserId, password: String, baseUrl: String, deviceDisplayName: String?) = httpRequest( path = "_matrix/client/r0/login", method = MatrixHttpClient.Method.POST, body = jsonBody( PasswordLoginRequest.serializer(), - PasswordLoginRequest(PasswordLoginRequest.UserIdentifier(userId), password), + PasswordLoginRequest(PasswordLoginRequest.UserIdentifier(userId), password, deviceDisplayName), MatrixHttpClient.jsonWithDefaults ), authenticated = false, @@ -81,6 +81,7 @@ data class ApiWellKnown( internal data class PasswordLoginRequest( @SerialName("identifier") val userName: UserIdentifier, @SerialName("password") val password: String, + @SerialName("initial_device_display_name") val deviceDisplayName: String?, @SerialName("type") val type: String = "m.login.password", ) { diff --git a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/DefaultAuthService.kt b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/DefaultAuthService.kt index c017b27..b2f30bb 100644 --- a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/DefaultAuthService.kt +++ b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/DefaultAuthService.kt @@ -1,6 +1,7 @@ package app.dapk.st.matrix.auth.internal import app.dapk.st.matrix.auth.AuthService +import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.HomeServerUrl import app.dapk.st.matrix.common.UserCredentials @@ -13,11 +14,12 @@ internal class DefaultAuthService( httpClient: MatrixHttpClient, credentialsStore: CredentialsStore, json: Json, + deviceDisplayNameGenerator: DeviceDisplayNameGenerator, ) : AuthService { private val fetchWellKnownUseCase = FetchWellKnownUseCaseImpl(httpClient, json) - private val loginUseCase = LoginWithUserPasswordUseCase(httpClient, credentialsStore, fetchWellKnownUseCase) - private val loginServerUseCase = LoginWithUserPasswordServerUseCase(httpClient, credentialsStore) + private val loginUseCase = LoginWithUserPasswordUseCase(httpClient, credentialsStore, fetchWellKnownUseCase, deviceDisplayNameGenerator) + private val loginServerUseCase = LoginWithUserPasswordServerUseCase(httpClient, credentialsStore, deviceDisplayNameGenerator) private val registerCase = RegisterUseCase(httpClient, credentialsStore, json, fetchWellKnownUseCase) override suspend fun login(request: AuthService.LoginRequest): AuthService.LoginResult { diff --git a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordServerUseCase.kt b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordServerUseCase.kt index aac15fe..a62b432 100644 --- a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordServerUseCase.kt +++ b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordServerUseCase.kt @@ -1,6 +1,7 @@ package app.dapk.st.matrix.auth.internal import app.dapk.st.matrix.auth.AuthService +import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.HomeServerUrl import app.dapk.st.matrix.common.UserCredentials @@ -10,6 +11,7 @@ import app.dapk.st.matrix.http.MatrixHttpClient class LoginWithUserPasswordServerUseCase( private val httpClient: MatrixHttpClient, private val credentialsProvider: CredentialsStore, + private val deviceDisplayNameGenerator: DeviceDisplayNameGenerator, ) { suspend fun login(userName: String, password: String, serverUrl: HomeServerUrl): AuthService.LoginResult { @@ -22,7 +24,7 @@ class LoginWithUserPasswordServerUseCase( } private suspend fun authenticate(baseUrl: HomeServerUrl, fullUserId: UserId, password: String): UserCredentials { - val authResponse = httpClient.execute(loginRequest(fullUserId, password, baseUrl.value)) + val authResponse = httpClient.execute(loginRequest(fullUserId, password, baseUrl.value, deviceDisplayNameGenerator.generate())) return UserCredentials( authResponse.accessToken, baseUrl, @@ -30,4 +32,4 @@ class LoginWithUserPasswordServerUseCase( authResponse.deviceId, ).also { credentialsProvider.update(it) } } -} \ No newline at end of file +} diff --git a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordUseCase.kt b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordUseCase.kt index 94eba51..f13e658 100644 --- a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordUseCase.kt +++ b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordUseCase.kt @@ -1,6 +1,7 @@ package app.dapk.st.matrix.auth.internal import app.dapk.st.matrix.auth.AuthService +import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.HomeServerUrl import app.dapk.st.matrix.common.UserCredentials @@ -14,6 +15,7 @@ class LoginWithUserPasswordUseCase( private val httpClient: MatrixHttpClient, private val credentialsProvider: CredentialsStore, private val fetchWellKnownUseCase: FetchWellKnownUseCase, + private val deviceDisplayNameGenerator: DeviceDisplayNameGenerator, ) { suspend fun login(userName: String, password: String): AuthService.LoginResult { @@ -27,6 +29,7 @@ class LoginWithUserPasswordUseCase( onFailure = { AuthService.LoginResult.Error(it) } ) } + WellKnownResult.InvalidWellKnown -> AuthService.LoginResult.MissingWellKnown WellKnownResult.MissingWellKnown -> AuthService.LoginResult.MissingWellKnown is WellKnownResult.Error -> AuthService.LoginResult.Error(wellKnownResult.cause) @@ -42,7 +45,7 @@ class LoginWithUserPasswordUseCase( } private suspend fun authenticate(baseUrl: HomeServerUrl, fullUserId: UserId, password: String): UserCredentials { - val authResponse = httpClient.execute(loginRequest(fullUserId, password, baseUrl.value)) + val authResponse = httpClient.execute(loginRequest(fullUserId, password, baseUrl.value, deviceDisplayNameGenerator.generate())) return UserCredentials( authResponse.accessToken, baseUrl, From 47be48579d8cffe9483a7f6af3d484c739844833 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 17:17:37 +0100 Subject: [PATCH 29/38] avoiding recreating navigation callback on recomposition --- .../app/dapk/st/directory/DirectoryListingScreen.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt index 95ed701..559fa8d 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt @@ -91,6 +91,7 @@ fun DirectoryScreen(directoryViewModel: DirectoryViewModel) { is Error -> GenericError { // TODO } + is Content -> Content(listState, state) } Toolbar(title = "Messages", offset = { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }) @@ -106,6 +107,7 @@ private fun DirectoryViewModel.ObserveEvents(listState: LazyListState, toolbarPo is OpenDownloadUrl -> { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.url))) } + DirectoryEvent.ScrollToTop -> { toolbarPosition.value = 0f listState.scrollToItem(0) @@ -121,8 +123,10 @@ val clock = Clock.systemUTC() @Composable private fun Content(listState: LazyListState, state: Content) { val context = LocalContext.current - val navigateToRoom = { roomId: RoomId -> - context.startActivity(MessengerActivity.newInstance(context, roomId)) + val navigateToRoom = remember { + { roomId: RoomId -> + context.startActivity(MessengerActivity.newInstance(context, roomId)) + } } val scope = rememberCoroutineScope() @@ -247,6 +251,7 @@ private fun body(overview: RoomOverview, secondaryText: Color, typing: SyncServi color = MaterialTheme.colorScheme.primary ) } + else -> when (val lastMessage = overview.lastMessage) { null -> { Text( @@ -256,6 +261,7 @@ private fun body(overview: RoomOverview, secondaryText: Color, typing: SyncServi color = secondaryText ) } + else -> { when (overview.isGroup) { true -> { @@ -268,6 +274,7 @@ private fun body(overview: RoomOverview, secondaryText: Color, typing: SyncServi color = secondaryText ) } + false -> { Text( overflow = TextOverflow.Ellipsis, From a1460b307d0fee91af480106d9d234f6bf8fbc3d Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 17:23:17 +0100 Subject: [PATCH 30/38] avoiding recalculating static toolbar height --- .../kotlin/app/dapk/st/directory/DirectoryListingScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt index 559fa8d..284f728 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt @@ -60,7 +60,8 @@ fun DirectoryScreen(directoryViewModel: DirectoryViewModel) { ) val toolbarHeight = 72.dp - val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() } + val density = LocalDensity.current + val toolbarHeightPx = remember { with(density) { toolbarHeight.roundToPx().toFloat() } } val toolbarOffsetHeightPx = remember { mutableStateOf(0f) } directoryViewModel.ObserveEvents(listState, toolbarOffsetHeightPx) From 215695a3cef91d89a87eb3e1a1d6b23e0524577b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 17:33:27 +0100 Subject: [PATCH 31/38] fixing random device id generator starting with the same values --- app/src/main/kotlin/app/dapk/st/graph/AppModule.kt | 3 +-- 1 file changed, 1 insertion(+), 2 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 d51c69d..4dc6890 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -521,8 +521,7 @@ internal class AndroidImageContentReader(private val contentResolver: ContentRes internal class SmallTalkDeviceNameGenerator : DeviceDisplayNameGenerator { override fun generate(): String { - val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') - val randomIdentifier = (1..4).map { allowedChars.random() }.joinToString("") + val randomIdentifier = (('A'..'Z') + ('a'..'z') + ('0'..'9')).shuffled().take(4).joinToString("") return "SmallTalk Android ($randomIdentifier)" } } \ No newline at end of file From f8783fd008134a141bb0c8a10578836230b4e029 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 2 Oct 2022 19:22:39 +0100 Subject: [PATCH 32/38] allowing the prefetching of local echos to fail --- app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt index 1b28443..4997d90 100644 --- a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt +++ b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt @@ -58,7 +58,7 @@ class SmallTalkApplication : Application(), ModuleProvider { storeModule.credentialsStore().credentials()?.let { featureModules.pushModule.pushTokenRegistrar().registerCurrentToken() } - storeModule.localEchoStore.preload() + runCatching { storeModule.localEchoStore.preload() } } applicationScope.launch { From ec608fd01071cb350c31ef20a90b0849f78f12ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 05:30:25 +0000 Subject: [PATCH 33/38] Bump ktorVer from 2.1.1 to 2.1.2 Bumps `ktorVer` from 2.1.1 to 2.1.2. Updates `ktor-client-android` from 2.1.1 to 2.1.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.1.1...2.1.2) Updates `ktor-client-core` from 2.1.1 to 2.1.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.1.1...2.1.2) Updates `ktor-client-serialization` from 2.1.1 to 2.1.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.1.1...2.1.2) Updates `ktor-serialization-kotlinx-json` from 2.1.1 to 2.1.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.1.1...2.1.2) Updates `ktor-client-logging-jvm` from 2.1.1 to 2.1.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.1.1...2.1.2) Updates `ktor-client-java` from 2.1.1 to 2.1.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.1.1...2.1.2) Updates `ktor-client-content-negotiation` from 2.1.1 to 2.1.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.1.1...2.1.2) --- updated-dependencies: - dependency-name: io.ktor:ktor-client-android dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-client-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-client-serialization dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-serialization-kotlinx-json dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-client-logging-jvm dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-client-java dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-client-content-negotiation dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 7fef836..a0d4757 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -100,7 +100,7 @@ ext.Dependencies.with { def kotlinVer = "1.7.10" def sqldelightVer = "1.5.3" def composeVer = "1.2.1" - def ktorVer = "2.1.1" + def ktorVer = "2.1.2" google = new DependenciesContainer() google.with { From 768cbc7eb252385974ca81944ce0434cc5a8d1b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 05:30:33 +0000 Subject: [PATCH 34/38] Bump coil-compose from 2.2.1 to 2.2.2 Bumps [coil-compose](https://github.com/coil-kt/coil) from 2.2.1 to 2.2.2. - [Release notes](https://github.com/coil-kt/coil/releases) - [Changelog](https://github.com/coil-kt/coil/blob/main/CHANGELOG.md) - [Commits](https://github.com/coil-kt/coil/compare/2.2.1...2.2.2) --- updated-dependencies: - dependency-name: io.coil-kt:coil-compose dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 7fef836..28ce795 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -140,7 +140,7 @@ ext.Dependencies.with { ktorJava = "io.ktor:ktor-client-java:${ktorVer}" ktorContentNegotiation = "io.ktor:ktor-client-content-negotiation:${ktorVer}" - coil = "io.coil-kt:coil-compose:2.2.1" + coil = "io.coil-kt:coil-compose:2.2.2" accompanistSystemuicontroller = "com.google.accompanist:accompanist-systemuicontroller:0.25.1" junit = "junit:junit:4.13.2" From 94c90edbb1d4419f7a3dd919f601f92cb2b97f5a Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 4 Oct 2022 19:17:46 +0100 Subject: [PATCH 35/38] auto rebasing isn't working correctly, reverting to merges --- tools/beta-release/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/beta-release/app.js b/tools/beta-release/app.js index 1cf37eb..14a439a 100644 --- a/tools/beta-release/app.js +++ b/tools/beta-release/app.js @@ -113,7 +113,7 @@ const enablePrAutoMerge = async (github, prNodeId) => { `, { pullRequestId: prNodeId, - mergeMethod: "REBASE" + mergeMethod: "MERGE" } ) } From 0a982ff53f0e1b65106ce75f91445d8a8a1102f8 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 4 Oct 2022 20:51:57 +0100 Subject: [PATCH 36/38] stripping out more html tags from text --- .../st/matrix/sync/internal/sync/RoomEventFactory.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt index 4dfcd7b..4ad293b 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt @@ -64,10 +64,17 @@ fun String.stripTags() = this } .trim() .replaceLinks() - .replace("", "") - .replace("", "") + .removeTag("p") + .removeTag("em") + .removeTag("strong") + .removeTag("code") + .removeTag("pre") .replace(""", "\"") .replace("'", "'") + .replace("
", "\n") + .replace("
", "\n") + +private fun String.removeTag(name: String) = this.replace("<$name>", "").replace("/$name>", "") private fun String.replaceLinks(): String { return this.indexOfOrNull("