Browse Source

Merge pull request #2565 from element-hq/feature/bma/userDataCache

Read user avatar from cache
feature/bma/fixNotificationContent
Benoit Marty 6 months ago committed by GitHub
parent
commit
fb64018110
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      changelog.d/2488.bugfix
  2. 14
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt
  3. 2
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt
  4. 7
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt
  5. 4
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
  6. 8
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt
  7. 2
      features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt
  8. 13
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
  9. 2
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
  10. 4
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
  11. 29
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt
  12. 38
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
  13. 5
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt
  14. 8
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
  15. 33
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentUser.kt
  16. 32
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
  17. 17
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
  18. 41
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt
  19. BIN
      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
  20. BIN
      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
changelog.d/2488.bugfix

@ -0,0 +1 @@ @@ -0,0 +1 @@
Use user avatar from cache if available.

14
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt

@ -24,7 +24,6 @@ import androidx.compose.runtime.getValue @@ -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 @@ -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( @@ -58,11 +55,10 @@ class PreferencesRootPresenter @Inject constructor(
) : Presenter<PreferencesRootState> {
@Composable
override fun present(): PreferencesRootState {
val matrixUser: MutableState<MatrixUser?> = 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( @@ -121,10 +117,6 @@ class PreferencesRootPresenter @Inject constructor(
)
}
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
matrixUser.value = matrixClient.getCurrentUser()
}
private fun CoroutineScope.initAccountManagementUrl(
accountManagementUrl: MutableState<String?>,
devicesManagementUrl: MutableState<String?>,

2
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 @@ -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,

7
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 @@ -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,

4
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt

@ -77,7 +77,7 @@ fun PreferencesRootView( @@ -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 @@ -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 = {},

8
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt

@ -75,7 +75,13 @@ class PreferencesRootPresenterTest { @@ -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(

2
features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt

@ -223,7 +223,7 @@ private class FakeRoomMemberListNavigator : RoomMemberListNavigator { @@ -223,7 +223,7 @@ private class FakeRoomMemberListNavigator : RoomMemberListNavigator {
var openRoomMemberDetailsCallCount = 0
private set
override fun openRoomMemberDetails(userId: UserId) {
override fun openRoomMemberDetails(roomMemberId: UserId) {
openRoomMemberDetailsCallCount++
}
}

13
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 @@ -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( @@ -101,16 +99,15 @@ class RoomListPresenter @Inject constructor(
override fun present(): RoomListState {
val coroutineScope = rememberCoroutineScope()
val leaveRoomState = leaveRoomPresenter.present()
val matrixUser: MutableState<MatrixUser?> = 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( @@ -157,10 +154,6 @@ class RoomListPresenter @Inject constructor(
)
}
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
matrixUser.value = client.getCurrentUser()
}
@Composable
private fun securityBannerState(
securityBannerDismissed: Boolean,

2
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt

@ -28,7 +28,7 @@ import kotlinx.collections.immutable.ImmutableList @@ -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?,

4
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt

@ -49,14 +49,14 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> { @@ -49,14 +49,14 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
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,

29
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 @@ -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 @@ -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 @@ -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( @@ -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( @@ -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( @@ -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( @@ -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)

38
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 @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -585,7 +586,6 @@ class RoomListPresenterTests {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java)
scope.cancel()
}

5
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt

@ -302,7 +302,7 @@ fun Modifier.bloom( @@ -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( @@ -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( @@ -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

8
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt

@ -42,6 +42,7 @@ import java.io.Closeable @@ -42,6 +42,7 @@ import java.io.Closeable
interface MatrixClient : Closeable {
val sessionId: SessionId
val deviceId: String
val userProfile: StateFlow<MatrixUser>
val roomListService: RoomListService
val mediaLoader: MatrixMediaLoader
val sessionCoroutineScope: CoroutineScope
@ -77,8 +78,11 @@ interface MatrixClient : Closeable { @@ -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<String>
suspend fun loadUserAvatarUrl(): Result<String?>
/**
* Retrieve the user profile, will also eventually emit a new value to [userProfile].
*/
suspend fun getUserProfile(): Result<MatrixUser>
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
fun roomMembershipObserver(): RoomMembershipObserver

33
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentUser.kt

@ -1,33 +0,0 @@ @@ -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,
)
}

32
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt

@ -73,7 +73,9 @@ import kotlinx.coroutines.CoroutineScope @@ -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( @@ -249,6 +251,17 @@ class RustMatrixClient(
private val clientDelegateTaskHandle: TaskHandle? = client.setDelegate(clientDelegate)
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(
MatrixUser(
userId = sessionId,
// TODO cache for displayName?
displayName = null,
avatarUrl = client.cachedAvatarUrl(),
)
)
override val userProfile: StateFlow<MatrixUser> = _userProfile
override val ignoredUsersFlow = mxCallbackFlow<ImmutableList<UserId>> {
client.subscribeToIgnoredUsers(object : IgnoredUsersListener {
override fun call(ignoredUserIds: List<String>) {
@ -265,6 +278,10 @@ class RustMatrixClient( @@ -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( @@ -374,6 +391,9 @@ class RustMatrixClient(
}
}
override suspend fun getUserProfile(): Result<MatrixUser> = getProfile(sessionId)
.onSuccess { _userProfile.tryEmit(it) }
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> =
withContext(sessionDispatcher) {
runCatching {
@ -471,18 +491,6 @@ class RustMatrixClient( @@ -471,18 +491,6 @@ class RustMatrixClient(
}
}
override suspend fun loadUserDisplayName(): Result<String> = withContext(sessionDispatcher) {
runCatching {
client.displayName()
}
}
override suspend fun loadUserAvatarUrl(): Result<String?> = withContext(sessionDispatcher) {
runCatching {
client.avatarUrl()
}
}
override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String> = withContext(sessionDispatcher) {
runCatching {
client.uploadMedia(mimeType, data, progressCallback?.toProgressWatcher())

17
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt

@ -48,14 +48,15 @@ import kotlinx.collections.immutable.persistentListOf @@ -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<String> = Result.success(A_USER_NAME),
private val userAvatarUrl: Result<String> = 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( @@ -73,6 +74,8 @@ class FakeMatrixClient(
var removeAvatarCalled: Boolean = false
private set
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl))
override val userProfile: StateFlow<MatrixUser> = _userProfile
override val ignoredUsersFlow: MutableStateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf())
private var ignoreUserResult: Result<Unit> = Result.success(Unit)
@ -140,12 +143,10 @@ class FakeMatrixClient( @@ -140,12 +143,10 @@ class FakeMatrixClient(
override fun close() = Unit
override suspend fun loadUserDisplayName(): Result<String> {
return userDisplayName
}
override suspend fun loadUserAvatarUrl(): Result<String?> {
return userAvatarUrl
override suspend fun getUserProfile(): Result<MatrixUser> = simulateLongTask {
val result = getProfileResults[sessionId]?.getOrNull() ?: MatrixUser(sessionId, userDisplayName, userAvatarUrl)
_userProfile.tryEmit(result)
return Result.success(result)
}
override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> {

41
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt

@ -292,23 +292,30 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -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)
}

BIN
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 (Stored with Git LFS)

Binary file not shown.

BIN
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 (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save