Show current user in the settings and extract code in CurrentUserProvider.

This commit is contained in:
Benoit Marty
2023-06-30 14:26:35 +02:00
committed by Benoit Marty
parent 0c29852d62
commit 002ddf4f3f
12 changed files with 226 additions and 69 deletions

View File

@@ -17,23 +17,38 @@
package io.element.android.features.preferences.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter
import io.element.android.features.logout.api.LogoutPreferencePresenter
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.matrix.api.user.CurrentUserProvider
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class PreferencesRootPresenter @Inject constructor(
private val logoutPresenter: LogoutPreferencePresenter,
private val rageshakePresenter: RageshakePreferencesPresenter,
private val analyticsPresenter: AnalyticsPreferencesPresenter,
private val currentUserProvider: CurrentUserProvider,
private val buildType: BuildType,
) : Presenter<PreferencesRootState> {
@Composable
override fun present(): PreferencesRootState {
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
mutableStateOf(null)
}
LaunchedEffect(Unit) {
initialLoad(matrixUser)
}
val logoutState = logoutPresenter.present()
val rageshakeState = rageshakePresenter.present()
val analyticsState = analyticsPresenter.present()
@@ -42,8 +57,12 @@ class PreferencesRootPresenter @Inject constructor(
logoutState = logoutState,
rageshakeState = rageshakeState,
analyticsState = analyticsState,
myUser = Async.Uninitialized,
myUser = matrixUser.value,
showDeveloperSettings = showDeveloperSettings
)
}
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
matrixUser.value = currentUserProvider.provide()
}
}

View File

@@ -19,13 +19,12 @@ package io.element.android.features.preferences.impl.root
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
import io.element.android.features.logout.api.LogoutPreferenceState
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.user.MatrixUser
data class PreferencesRootState(
val logoutState: LogoutPreferenceState,
val rageshakeState: RageshakePreferencesState,
val analyticsState: AnalyticsPreferencesState,
val myUser: Async<MatrixUser>,
val myUser: MatrixUser?,
val showDeveloperSettings: Boolean
)

View File

@@ -19,12 +19,11 @@ package io.element.android.features.preferences.impl.root
import io.element.android.features.analytics.api.preferences.aAnalyticsPreferencesState
import io.element.android.features.logout.api.aLogoutPreferenceState
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
import io.element.android.libraries.architecture.Async
fun aPreferencesRootState() = PreferencesRootState(
logoutState = aLogoutPreferenceState(),
rageshakeState = aRageshakePreferencesState(),
analyticsState = aAnalyticsPreferencesState(),
myUser = Async.Uninitialized,
myUser = null,
showDeveloperSettings = true
)

View File

@@ -92,5 +92,5 @@ fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class)
@Composable
private fun ContentToPreview(matrixUser: MatrixUser) {
PreferencesRootView(aPreferencesRootState().copy(myUser = Async.Success(matrixUser)))
PreferencesRootView(aPreferencesRootState().copy(myUser = matrixUser))
}

View File

@@ -16,14 +16,10 @@
package io.element.android.features.preferences.impl.user
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -32,16 +28,13 @@ import io.element.android.libraries.matrix.ui.components.MatrixUserWithNullProvi
@Composable
fun UserPreferences(
user: Async<MatrixUser>,
user: MatrixUser?,
modifier: Modifier = Modifier,
) {
when (val userData = user.dataOrNull()) {
null -> Spacer(modifier = modifier.height(1.dp))
else -> MatrixUserHeader(
modifier = modifier,
matrixUser = userData
)
}
MatrixUserHeader(
modifier = modifier,
matrixUser = user
)
}
@Preview
@@ -56,9 +49,5 @@ internal fun UserPreferencesDarkPreview(@PreviewParameter(MatrixUserWithNullProv
@Composable
private fun ContentToPreview(matrixUser: MatrixUser?) {
if (matrixUser == null) {
UserPreferences(Async.Uninitialized)
} else {
UserPreferences(Async.Success(matrixUser))
}
UserPreferences(matrixUser)
}

View File

@@ -28,6 +28,10 @@ import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePr
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.user.CurrentUserProvider
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -35,27 +39,36 @@ import org.junit.Test
class PreferencesRootPresenterTest {
@Test
fun `present - initial state`() = runTest {
val logoutPresenter = DefaultLogoutPreferencePresenter(FakeMatrixClient())
val matrixClient = FakeMatrixClient()
val logoutPresenter = DefaultLogoutPreferencePresenter(matrixClient)
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val analyticsPresenter = DefaultAnalyticsPreferencesPresenter(FakeAnalyticsService(), A_BUILD_META)
val presenter = PreferencesRootPresenter(
logoutPresenter,
rageshakePresenter,
analyticsPresenter,
CurrentUserProvider(matrixClient),
A_BUILD_META.buildType
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.logoutState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.analyticsState.isEnabled).isFalse()
assertThat(initialState.rageshakeState.isEnabled).isTrue()
assertThat(initialState.rageshakeState.isSupported).isTrue()
assertThat(initialState.rageshakeState.sensitivity).isEqualTo(1.0f)
assertThat(initialState.myUser).isEqualTo(Async.Uninitialized)
assertThat(initialState.showDeveloperSettings).isEqualTo(true)
assertThat(initialState.myUser).isNull()
val loadedState = awaitItem()
assertThat(loadedState.logoutState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(loadedState.analyticsState.isEnabled).isFalse()
assertThat(loadedState.rageshakeState.isEnabled).isTrue()
assertThat(loadedState.rageshakeState.isSupported).isTrue()
assertThat(loadedState.rageshakeState.sensitivity).isEqualTo(1.0f)
assertThat(loadedState.myUser).isEqualTo(
MatrixUser(
userId = matrixClient.sessionId,
displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL
)
)
assertThat(loadedState.showDeveloperSettings).isEqualTo(true)
}
}
}

View File

@@ -26,10 +26,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
import io.element.android.libraries.architecture.Presenter
@@ -43,8 +43,8 @@ import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsS
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomSummary
import io.element.android.libraries.matrix.api.user.CurrentUserProvider
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
@@ -60,6 +60,7 @@ private const val extendedRangeSize = 40
class RoomListPresenter @Inject constructor(
private val client: MatrixClient,
private val currentUserProvider: CurrentUserProvider,
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
private val roomLastMessageFormatter: RoomLastMessageFormatter,
private val sessionVerificationService: SessionVerificationService,
@@ -162,13 +163,7 @@ class RoomListPresenter @Inject constructor(
}
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
val userDisplayName = client.loadUserDisplayName().getOrNull()
matrixUser.value = MatrixUser(
userId = UserId(client.sessionId.value),
displayName = userDisplayName,
avatarUrl = userAvatarUrl,
)
matrixUser.value = currentUserProvider.provide()
}
private fun updateVisibleRange(range: IntRange) {

View File

@@ -31,6 +31,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
import io.element.android.libraries.matrix.api.user.CurrentUserProvider
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EXCEPTION
@@ -50,8 +51,10 @@ class RoomListPresenterTests {
@Test
fun `present - should start with no user and then load user with success`() = runTest {
val matrixClient = FakeMatrixClient()
val presenter = RoomListPresenter(
FakeMatrixClient(),
matrixClient,
CurrentUserProvider(matrixClient),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@@ -75,11 +78,13 @@ class RoomListPresenterTests {
@Test
fun `present - should start with no user and then load user with error`() = runTest {
val matrixClient = FakeMatrixClient(
userDisplayName = Result.failure(AN_EXCEPTION),
userAvatarURLString = Result.failure(AN_EXCEPTION),
)
val presenter = RoomListPresenter(
FakeMatrixClient(
userDisplayName = Result.failure(AN_EXCEPTION),
userAvatarURLString = Result.failure(AN_EXCEPTION),
),
matrixClient,
CurrentUserProvider(matrixClient),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@@ -100,8 +105,10 @@ class RoomListPresenterTests {
@Test
fun `present - should filter room with success`() = runTest {
val matrixClient = FakeMatrixClient()
val presenter = RoomListPresenter(
FakeMatrixClient(),
matrixClient,
CurrentUserProvider(matrixClient),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@@ -127,10 +134,12 @@ class RoomListPresenterTests {
@Test
fun `present - load 1 room with success`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
)
val presenter = RoomListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
),
matrixClient,
CurrentUserProvider(matrixClient),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@@ -159,10 +168,12 @@ class RoomListPresenterTests {
@Test
fun `present - load 1 room with success and filter rooms`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
)
val presenter = RoomListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
),
matrixClient,
CurrentUserProvider(matrixClient),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@@ -197,10 +208,12 @@ class RoomListPresenterTests {
@Test
fun `present - update visible range`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
)
val presenter = RoomListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
),
matrixClient,
CurrentUserProvider(matrixClient),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@@ -245,10 +258,12 @@ class RoomListPresenterTests {
@Test
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
)
val presenter = RoomListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
),
matrixClient,
CurrentUserProvider(matrixClient),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService().apply {
@@ -274,8 +289,10 @@ class RoomListPresenterTests {
@Test
fun `present - sets invite state`() = runTest {
val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites)
val matrixClient = FakeMatrixClient()
val presenter = RoomListPresenter(
FakeMatrixClient(),
matrixClient,
CurrentUserProvider(matrixClient),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@@ -304,8 +321,10 @@ class RoomListPresenterTests {
@Test
fun `present - show context menu`() = runTest {
val matrixClient = FakeMatrixClient()
val presenter = RoomListPresenter(
FakeMatrixClient(),
matrixClient,
CurrentUserProvider(matrixClient),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@@ -331,8 +350,10 @@ class RoomListPresenterTests {
@Test
fun `present - hide context menu`() = runTest {
val matrixClient = FakeMatrixClient()
val presenter = RoomListPresenter(
FakeMatrixClient(),
matrixClient,
CurrentUserProvider(matrixClient),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@@ -363,8 +384,10 @@ class RoomListPresenterTests {
@Test
fun `present - leave room calls into leave room presenter`() = runTest {
val leaveRoomPresenter = LeaveRoomPresenterFake()
val matrixClient = FakeMatrixClient()
val presenter = RoomListPresenter(
FakeMatrixClient(),
matrixClient,
CurrentUserProvider(matrixClient),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.user
import io.element.android.libraries.matrix.api.MatrixClient
import javax.inject.Inject
class CurrentUserProvider @Inject constructor(
private val matrixClient: MatrixClient,
) {
suspend fun provide(): MatrixUser {
val userAvatarUrl = matrixClient.loadUserAvatarURLString().getOrNull()
val userDisplayName = matrixClient.loadUserDisplayName().getOrNull()
return MatrixUser(
userId = matrixClient.sessionId,
displayName = userDisplayName,
avatarUrl = userAvatarUrl,
)
}
}

View File

@@ -16,7 +16,6 @@
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -42,13 +41,16 @@ import io.element.android.libraries.theme.ElementTheme
@Composable
fun MatrixUserHeader(
matrixUser: MatrixUser,
matrixUser: MatrixUser?,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
// onClick: () -> Unit = {},
) {
if (matrixUser == null) {
return MatrixUserHeaderPlaceholder(modifier = modifier)
}
Row(
modifier = modifier
.clickable(onClick = onClick)
// .clickable(onClick = onClick)
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically

View File

@@ -0,0 +1,82 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.roomListPlaceholder
import io.element.android.libraries.theme.ElementTheme
@Composable
fun MatrixUserHeaderPlaceholder(
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.padding(vertical = 12.dp)
.size(AvatarSize.UserPreference.dp)
.background(color = ElementTheme.colors.roomListPlaceholder, shape = CircleShape)
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
PlaceholderAtom(width = 80.dp, height = 7.dp)
Spacer(modifier = Modifier.height(16.dp))
PlaceholderAtom(width = 180.dp, height = 6.dp)
}
}
}
@Preview
@Composable
fun MatrixUserHeaderPlaceholderLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
fun MatrixUserHeaderPlaceholderDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
MatrixUserHeaderPlaceholder()
}

View File

@@ -38,6 +38,7 @@ import io.element.android.libraries.eventformatter.impl.StateContentFormatter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.user.CurrentUserProvider
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -60,6 +61,7 @@ class RoomListScreen(
private val stringProvider = AndroidStringProvider(context.resources)
private val presenter = RoomListPresenter(
client = matrixClient,
currentUserProvider = CurrentUserProvider(matrixClient),
lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters),
roomLastMessageFormatter = DefaultRoomLastMessageFormatter(
sp = stringProvider,