diff --git a/changelog.d/2488.bugfix b/changelog.d/2488.bugfix new file mode 100644 index 0000000000..1627a350a2 --- /dev/null +++ b/changelog.d/2488.bugfix @@ -0,0 +1 @@ +Use user avatar from cache if available. diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index 18af3a9b6d..f80ee11aba 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.features.logout.api.direct.DirectLogoutPresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildType @@ -35,8 +34,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.oidc.AccountManagementAction -import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.libraries.matrix.api.user.getCurrentUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope @@ -58,11 +55,10 @@ class PreferencesRootPresenter @Inject constructor( ) : Presenter { @Composable override fun present(): PreferencesRootState { - val matrixUser: MutableState = rememberSaveable { - mutableStateOf(null) - } + val matrixUser = matrixClient.userProfile.collectAsState() LaunchedEffect(Unit) { - initialLoad(matrixUser) + // Force a refresh of the profile + matrixClient.getUserProfile() } val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() @@ -121,10 +117,6 @@ class PreferencesRootPresenter @Inject constructor( ) } - private fun CoroutineScope.initialLoad(matrixUser: MutableState) = launch { - matrixUser.value = matrixClient.getCurrentUser() - } - private fun CoroutineScope.initAccountManagementUrl( accountManagementUrl: MutableState, devicesManagementUrl: MutableState, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index 7ac73b10a9..eabdc80da0 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -21,7 +21,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.user.MatrixUser data class PreferencesRootState( - val myUser: MatrixUser?, + val myUser: MatrixUser, val version: String, val deviceId: String?, val showCompleteVerification: Boolean, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt index 0f027d775d..b688a493b6 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt @@ -18,10 +18,13 @@ package io.element.android.features.preferences.impl.root import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings -fun aPreferencesRootState() = PreferencesRootState( - myUser = null, +fun aPreferencesRootState( + myUser: MatrixUser, +) = PreferencesRootState( + myUser = myUser, version = "Version 1.1 (1)", deviceId = "ILAKNDNASDLK", showCompleteVerification = true, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 835cd6cc15..293889e868 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -77,7 +77,7 @@ fun PreferencesRootView( ) { UserPreferences( modifier = Modifier.clickable { - state.myUser?.let(onOpenUserProfile) + onOpenUserProfile(state.myUser) }, user = state.myUser, ) @@ -225,7 +225,7 @@ internal fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider @Composable private fun ContentToPreview(matrixUser: MatrixUser) { PreferencesRootView( - state = aPreferencesRootState().copy(myUser = matrixUser), + state = aPreferencesRootState(myUser = matrixUser), onBackPressed = {}, onOpenAnalytics = {}, onOpenRageShake = {}, diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index f9789639f3..3288e94eb4 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -75,7 +75,13 @@ class PreferencesRootPresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.myUser).isNull() + assertThat(initialState.myUser).isEqualTo( + MatrixUser( + userId = matrixClient.sessionId, + displayName = A_USER_NAME, + avatarUrl = AN_AVATAR_URL + ) + ) assertThat(initialState.version).isEqualTo("A Version") val loadedState = awaitItem() assertThat(loadedState.myUser).isEqualTo( diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index bc7dff92b1..33f0b3fec0 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -223,7 +223,7 @@ private class FakeRoomMemberListNavigator : RoomMemberListNavigator { var openRoomMemberDetailsCallCount = 0 private set - override fun openRoomMemberDetails(userId: UserId) { + override fun openRoomMemberDetails(roomMemberId: UserId) { openRoomMemberDetailsCallCount++ } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 5197361f2d..900e640981 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -58,8 +58,6 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.timeline.ReceiptType -import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.libraries.matrix.api.user.getCurrentUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction @@ -101,16 +99,15 @@ class RoomListPresenter @Inject constructor( override fun present(): RoomListState { val coroutineScope = rememberCoroutineScope() val leaveRoomState = leaveRoomPresenter.present() - val matrixUser: MutableState = rememberSaveable { - mutableStateOf(null) - } + val matrixUser = client.userProfile.collectAsState() val networkConnectionStatus by networkMonitor.connectivity.collectAsState() val filtersState = filtersPresenter.present() val searchState = searchPresenter.present() LaunchedEffect(Unit) { roomListDataSource.launchIn(this) - initialLoad(matrixUser) + // Force a refresh of the profile + client.getUserProfile() } var securityBannerDismissed by rememberSaveable { mutableStateOf(false) } @@ -157,10 +154,6 @@ class RoomListPresenter @Inject constructor( ) } - private fun CoroutineScope.initialLoad(matrixUser: MutableState) = launch { - matrixUser.value = client.getCurrentUser() - } - @Composable private fun securityBannerState( securityBannerDismissed: Boolean, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 73a1460ad9..250f1c8e87 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -28,7 +28,7 @@ import kotlinx.collections.immutable.ImmutableList @Immutable data class RoomListState( - val matrixUser: MatrixUser?, + val matrixUser: MatrixUser, val showAvatarIndicator: Boolean, val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 3d577d9ea9..00f6833f02 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -49,14 +49,14 @@ open class RoomListStateProvider : PreviewParameterProvider { aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)), aRoomListState(contentState = anEmptyContentState()), aRoomListState(contentState = aSkeletonContentState()), - aRoomListState(matrixUser = null, contentState = aMigrationContentState()), + aRoomListState(matrixUser = MatrixUser(userId = UserId("@id:domain")), contentState = aMigrationContentState()), aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")), aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)), ) } internal fun aRoomListState( - matrixUser: MatrixUser? = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), + matrixUser: MatrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), showAvatarIndicator: Boolean = false, hasNetworkConnection: Boolean = true, snackbarMessage: SnackbarMessage? = null, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt index 50b4144225..301a32a26c 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt @@ -21,10 +21,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TopAppBarDefaults @@ -73,7 +71,6 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalDivi import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar -import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -87,7 +84,7 @@ private val avatarBloomSize = 430.dp @OptIn(ExperimentalMaterial3Api::class) @Composable fun RoomListTopBar( - matrixUser: MatrixUser?, + matrixUser: MatrixUser, showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, onToggleSearch: () -> Unit, @@ -117,7 +114,7 @@ fun RoomListTopBar( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun DefaultRoomListTopBar( - matrixUser: MatrixUser?, + matrixUser: MatrixUser, showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, scrollBehavior: TopAppBarScrollBehavior, @@ -142,7 +139,7 @@ private fun DefaultRoomListTopBar( val avatarData by remember(matrixUser) { derivedStateOf { - matrixUser?.getAvatarData(size = AvatarSize.CurrentUserTopBar) + matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar) } } @@ -295,7 +292,7 @@ private fun DefaultRoomListTopBar( @Composable private fun NavigationIcon( - avatarData: AvatarData?, + avatarData: AvatarData, showAvatarIndicator: Boolean, onClick: () -> Unit, ) { @@ -304,20 +301,10 @@ private fun NavigationIcon( onClick = onClick, ) { Box { - if (avatarData != null) { - Avatar( - avatarData = avatarData, - contentDescription = stringResource(CommonStrings.common_settings), - ) - } else { - // Placeholder avatar until the avatarData is available - Surface( - modifier = Modifier.size(AvatarSize.CurrentUserTopBar.dp), - shape = CircleShape, - color = ElementTheme.colors.iconSecondary, - content = {} - ) - } + Avatar( + avatarData = avatarData, + contentDescription = stringResource(CommonStrings.common_settings), + ) if (showAvatarIndicator) { RedIndicatorAtom( modifier = Modifier.align(Alignment.TopEnd) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index beffffa8ef..6b5877a010 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -56,6 +56,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.timeline.ReceiptType +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.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -93,17 +94,24 @@ class RoomListPresenterTests { @Test fun `present - should start with no user and then load user with success`() = runTest { val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(coroutineScope = scope) + val matrixClient = FakeMatrixClient( + userDisplayName = null, + userAvatarUrl = null, + ) + matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL))) + val presenter = createRoomListPresenter( + client = matrixClient, + coroutineScope = scope, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.matrixUser).isNull() + assertThat(initialState.matrixUser).isEqualTo(MatrixUser(A_USER_ID)) val withUserState = awaitItem() - assertThat(withUserState.matrixUser).isNotNull() - assertThat(withUserState.matrixUser!!.userId).isEqualTo(A_USER_ID) - assertThat(withUserState.matrixUser!!.displayName).isEqualTo(A_USER_NAME) - assertThat(withUserState.matrixUser!!.avatarUrl).isEqualTo(AN_AVATAR_URL) + assertThat(withUserState.matrixUser.userId).isEqualTo(A_USER_ID) + assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME) + assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(withUserState.showAvatarIndicator).isTrue() scope.cancel() } @@ -128,7 +136,6 @@ class RoomListPresenterTests { val initialState = awaitItem() assertThat(initialState.showAvatarIndicator).isTrue() sessionVerificationService.givenCanVerifySession(false) - assertThat(awaitItem().showAvatarIndicator).isTrue() encryptionService.emitBackupState(BackupState.ENABLED) val finalState = awaitItem() assertThat(finalState.showAvatarIndicator).isFalse() @@ -139,19 +146,18 @@ 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), - userAvatarUrl = Result.failure(AN_EXCEPTION), + userDisplayName = null, + userAvatarUrl = null, ) + matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION)) val scope = CoroutineScope(coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.matrixUser).isNull() - val withUserState = awaitItem() - assertThat(withUserState.matrixUser).isNotNull() - scope.cancel() + assertThat(initialState.matrixUser).isEqualTo(MatrixUser(matrixClient.sessionId)) + // No new state is coming } } @@ -364,7 +370,6 @@ class RoomListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() val summary = createRoomListRoomSummary() initialState.eventSink(RoomListEvents.ShowContextMenu(summary)) @@ -414,8 +419,6 @@ class RoomListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) - val initialState = awaitItem() val summary = createRoomListRoomSummary() initialState.eventSink(RoomListEvents.ShowContextMenu(summary)) @@ -473,7 +476,6 @@ class RoomListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() eventRecorder.assertEmpty() initialState.eventSink(RoomListEvents.ToggleSearchResults) @@ -558,7 +560,6 @@ class RoomListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() // The migration screen is shown if the migration screen has not been shown before assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Migration::class.java) @@ -585,7 +586,6 @@ class RoomListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java) scope.cancel() } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt index ae844a1a45..8558780a22 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt @@ -302,7 +302,7 @@ fun Modifier.bloom( /** * Bloom effect modifier for avatars. Applies a bloom effect to the component. * @param avatarData The avatar data to use as the bloom source. - * If the avatar data has a URL it will be used as the bloom source, otherwise the initials will be used. If `null` is passed, no bloom effect will be applied. + * If the avatar data has a URL it will be used as the bloom source, otherwise the initials will be used. * @param background The background color to use for the bloom effect. Since we use blend modes it must be non-transparent. * @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component. * @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component. @@ -313,7 +313,7 @@ fun Modifier.bloom( * @param alpha The alpha value to apply to the bloom effect. */ fun Modifier.avatarBloom( - avatarData: AvatarData?, + avatarData: AvatarData, background: Color, blurSize: DpSize = DpSize.Unspecified, offset: DpOffset = DpOffset.Unspecified, @@ -327,7 +327,6 @@ fun Modifier.avatarBloom( ) = composed { // Bloom only works on API 29+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this - avatarData ?: return@composed this // Request the avatar contents to use as the bloom source val context = LocalContext.current diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 49e30e5158..955c8d72fa 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -42,6 +42,7 @@ import java.io.Closeable interface MatrixClient : Closeable { val sessionId: SessionId val deviceId: String + val userProfile: StateFlow val roomListService: RoomListService val mediaLoader: MatrixMediaLoader val sessionCoroutineScope: CoroutineScope @@ -77,8 +78,11 @@ interface MatrixClient : Closeable { * @param ignoreSdkError if true, the SDK will ignore any error and delete the session data anyway. */ suspend fun logout(ignoreSdkError: Boolean): String? - suspend fun loadUserDisplayName(): Result - suspend fun loadUserAvatarUrl(): Result + + /** + * Retrieve the user profile, will also eventually emit a new value to [userProfile]. + */ + suspend fun getUserProfile(): Result suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result fun roomMembershipObserver(): RoomMembershipObserver diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentUser.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentUser.kt deleted file mode 100644 index 56b61e4cde..0000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentUser.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 - -/** - * Get the current user, as [MatrixUser], using [MatrixClient.loadUserAvatarUrl] - * and [MatrixClient.loadUserDisplayName]. - */ -suspend fun MatrixClient.getCurrentUser(): MatrixUser { - val userAvatarUrl = loadUserAvatarUrl().getOrNull() - val userDisplayName = loadUserDisplayName().getOrNull() - return MatrixUser( - userId = sessionId, - displayName = userDisplayName, - avatarUrl = userAvatarUrl, - ) -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index c27b75361b..31e439bbec 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -73,7 +73,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first @@ -249,6 +251,17 @@ class RustMatrixClient( private val clientDelegateTaskHandle: TaskHandle? = client.setDelegate(clientDelegate) + private val _userProfile: MutableStateFlow = MutableStateFlow( + MatrixUser( + userId = sessionId, + // TODO cache for displayName? + displayName = null, + avatarUrl = client.cachedAvatarUrl(), + ) + ) + + override val userProfile: StateFlow = _userProfile + override val ignoredUsersFlow = mxCallbackFlow> { client.subscribeToIgnoredUsers(object : IgnoredUsersListener { override fun call(ignoredUserIds: List) { @@ -265,6 +278,10 @@ class RustMatrixClient( setupVerificationControllerIfNeeded() } }.launchIn(sessionCoroutineScope) + sessionCoroutineScope.launch { + // Force a refresh of the profile + getUserProfile() + } } override suspend fun getRoom(roomId: RoomId): MatrixRoom? = withContext(sessionDispatcher) { @@ -374,6 +391,9 @@ class RustMatrixClient( } } + override suspend fun getUserProfile(): Result = getProfile(sessionId) + .onSuccess { _userProfile.tryEmit(it) } + override suspend fun searchUsers(searchTerm: String, limit: Long): Result = withContext(sessionDispatcher) { runCatching { @@ -471,18 +491,6 @@ class RustMatrixClient( } } - override suspend fun loadUserDisplayName(): Result = withContext(sessionDispatcher) { - runCatching { - client.displayName() - } - } - - override suspend fun loadUserAvatarUrl(): Result = withContext(sessionDispatcher) { - runCatching { - client.avatarUrl() - } - } - override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result = withContext(sessionDispatcher) { runCatching { client.uploadMedia(mimeType, data, progressCallback?.toProgressWatcher()) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 7b3e09e3e3..d7c3f71e64 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -48,14 +48,15 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.test.TestScope class FakeMatrixClient( override val sessionId: SessionId = A_SESSION_ID, override val deviceId: String = "A_DEVICE_ID", override val sessionCoroutineScope: CoroutineScope = TestScope(), - private val userDisplayName: Result = Result.success(A_USER_NAME), - private val userAvatarUrl: Result = Result.success(AN_AVATAR_URL), + private val userDisplayName: String? = A_USER_NAME, + private val userAvatarUrl: String? = AN_AVATAR_URL, override val roomListService: RoomListService = FakeRoomListService(), override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(), private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), @@ -73,6 +74,8 @@ class FakeMatrixClient( var removeAvatarCalled: Boolean = false private set + private val _userProfile: MutableStateFlow = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl)) + override val userProfile: StateFlow = _userProfile override val ignoredUsersFlow: MutableStateFlow> = MutableStateFlow(persistentListOf()) private var ignoreUserResult: Result = Result.success(Unit) @@ -140,12 +143,10 @@ class FakeMatrixClient( override fun close() = Unit - override suspend fun loadUserDisplayName(): Result { - return userDisplayName - } - - override suspend fun loadUserAvatarUrl(): Result { - return userAvatarUrl + override suspend fun getUserProfile(): Result = simulateLongTask { + val result = getProfileResults[sessionId]?.getOrNull() ?: MatrixUser(sessionId, userDisplayName, userAvatarUrl) + _userProfile.tryEmit(result) + return Result.success(result) } override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 9d12d7bd6c..7c9a60c279 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -292,23 +292,30 @@ class DefaultNotificationDrawerManager @Inject constructor( eventsForSessions.forEach { (sessionId, notifiableEvents) -> val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow() val imageLoader = imageLoaderHolder.get(client) - val currentUser = tryOrNull( - onError = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") }, - operation = { - // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash - val myUserDisplayName = client.loadUserDisplayName().getOrNull() ?: sessionId.value - val userAvatarUrl = client.loadUserAvatarUrl().getOrNull() - MatrixUser( - userId = sessionId, - displayName = myUserDisplayName, - avatarUrl = userAvatarUrl - ) - } - ) ?: MatrixUser( - userId = sessionId, - displayName = sessionId.value, - avatarUrl = null - ) + val userFromCache = client.userProfile.value + val currentUser = if (userFromCache.avatarUrl != null && userFromCache.displayName.isNullOrEmpty().not()) { + // We have an avatar and a display name, use it + userFromCache + } else { + tryOrNull( + onError = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") }, + operation = { + client.getUserProfile().getOrNull() + ?.let { + // displayName cannot be empty else NotificationCompat.MessagingStyle() will crash + if (it.displayName.isNullOrEmpty()) { + it.copy(displayName = sessionId.value) + } else { + it + } + } + } + ) ?: MatrixUser( + userId = sessionId, + displayName = sessionId.value, + avatarUrl = null + ) + } notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents, imageLoader) } diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en].png index 23df601373..1ff067e54b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0151879112a55b3b310aaf0ed33a4409597572bd78063f6617e8869d95cb5362 -size 137048 +oid sha256:0197c643bbbbc46dbb58ac8911b61003bb09fceb15f1997cba64bcb00aee90fe +size 162642 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en].png index 106eadb403..043a1223a4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9723e58db10eb39c1b63a4c916e9d5d6c22de934945eec20703ca43f0f189f8 -size 159766 +oid sha256:4b167704c2d05c857715aa11c3d7fb3abdf3b59ad7d6d2c479936d934a67f32d +size 186756