Use client.cachedAvatarUrl(), to be able to render the user avatar faster, and even if there is no network (#2488).
This commit is contained in:
parent
5c540c4af9
commit
1144d467e5
15 changed files with 105 additions and 124 deletions
|
|
@ -24,7 +24,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
|
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
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.indicator.api.IndicatorService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
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.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -58,11 +55,10 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
) : Presenter<PreferencesRootState> {
|
) : Presenter<PreferencesRootState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PreferencesRootState {
|
override fun present(): PreferencesRootState {
|
||||||
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
|
val matrixUser = matrixClient.userProfile.collectAsState()
|
||||||
mutableStateOf(null)
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
initialLoad(matrixUser)
|
// Force a refresh of the profile
|
||||||
|
matrixClient.getUserProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||||
|
|
@ -121,10 +117,6 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
|
|
||||||
matrixUser.value = matrixClient.getCurrentUser()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CoroutineScope.initAccountManagementUrl(
|
private fun CoroutineScope.initAccountManagementUrl(
|
||||||
accountManagementUrl: MutableState<String?>,
|
accountManagementUrl: MutableState<String?>,
|
||||||
devicesManagementUrl: MutableState<String?>,
|
devicesManagementUrl: MutableState<String?>,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
data class PreferencesRootState(
|
data class PreferencesRootState(
|
||||||
val myUser: MatrixUser?,
|
val myUser: MatrixUser,
|
||||||
val version: String,
|
val version: String,
|
||||||
val deviceId: String?,
|
val deviceId: String?,
|
||||||
val showCompleteVerification: Boolean,
|
val showCompleteVerification: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -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.features.logout.api.direct.aDirectLogoutState
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
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
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
fun aPreferencesRootState() = PreferencesRootState(
|
fun aPreferencesRootState(
|
||||||
myUser = null,
|
myUser: MatrixUser,
|
||||||
|
) = PreferencesRootState(
|
||||||
|
myUser = myUser,
|
||||||
version = "Version 1.1 (1)",
|
version = "Version 1.1 (1)",
|
||||||
deviceId = "ILAKNDNASDLK",
|
deviceId = "ILAKNDNASDLK",
|
||||||
showCompleteVerification = true,
|
showCompleteVerification = true,
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ fun PreferencesRootView(
|
||||||
) {
|
) {
|
||||||
UserPreferences(
|
UserPreferences(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
state.myUser?.let(onOpenUserProfile)
|
onOpenUserProfile(state.myUser)
|
||||||
},
|
},
|
||||||
user = state.myUser,
|
user = state.myUser,
|
||||||
)
|
)
|
||||||
|
|
@ -225,7 +225,7 @@ internal fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(matrixUser: MatrixUser) {
|
private fun ContentToPreview(matrixUser: MatrixUser) {
|
||||||
PreferencesRootView(
|
PreferencesRootView(
|
||||||
state = aPreferencesRootState().copy(myUser = matrixUser),
|
state = aPreferencesRootState(myUser = matrixUser),
|
||||||
onBackPressed = {},
|
onBackPressed = {},
|
||||||
onOpenAnalytics = {},
|
onOpenAnalytics = {},
|
||||||
onOpenRageShake = {},
|
onOpenRageShake = {},
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,13 @@ class PreferencesRootPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
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")
|
assertThat(initialState.version).isEqualTo("A Version")
|
||||||
val loadedState = awaitItem()
|
val loadedState = awaitItem()
|
||||||
assertThat(loadedState.myUser).isEqualTo(
|
assertThat(loadedState.myUser).isEqualTo(
|
||||||
|
|
|
||||||
|
|
@ -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.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
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.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.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||||
|
|
@ -101,16 +99,15 @@ class RoomListPresenter @Inject constructor(
|
||||||
override fun present(): RoomListState {
|
override fun present(): RoomListState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val leaveRoomState = leaveRoomPresenter.present()
|
val leaveRoomState = leaveRoomPresenter.present()
|
||||||
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
|
val matrixUser = client.userProfile.collectAsState()
|
||||||
mutableStateOf(null)
|
|
||||||
}
|
|
||||||
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
|
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
|
||||||
val filtersState = filtersPresenter.present()
|
val filtersState = filtersPresenter.present()
|
||||||
val searchState = searchPresenter.present()
|
val searchState = searchPresenter.present()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
roomListDataSource.launchIn(this)
|
roomListDataSource.launchIn(this)
|
||||||
initialLoad(matrixUser)
|
// Force a refresh of the profile
|
||||||
|
client.getUserProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
|
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
@ -157,10 +154,6 @@ class RoomListPresenter @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
|
|
||||||
matrixUser.value = client.getCurrentUser()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun securityBannerState(
|
private fun securityBannerState(
|
||||||
securityBannerDismissed: Boolean,
|
securityBannerDismissed: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class RoomListState(
|
data class RoomListState(
|
||||||
val matrixUser: MatrixUser?,
|
val matrixUser: MatrixUser,
|
||||||
val showAvatarIndicator: Boolean,
|
val showAvatarIndicator: Boolean,
|
||||||
val hasNetworkConnection: Boolean,
|
val hasNetworkConnection: Boolean,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,14 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
||||||
aRoomListState(contentState = anEmptyContentState()),
|
aRoomListState(contentState = anEmptyContentState()),
|
||||||
aRoomListState(contentState = aSkeletonContentState()),
|
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(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||||
aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)),
|
aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun aRoomListState(
|
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,
|
showAvatarIndicator: Boolean = false,
|
||||||
hasNetworkConnection: Boolean = true,
|
hasNetworkConnection: Boolean = true,
|
||||||
snackbarMessage: SnackbarMessage? = null,
|
snackbarMessage: SnackbarMessage? = null,
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ private val avatarBloomSize = 430.dp
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomListTopBar(
|
fun RoomListTopBar(
|
||||||
matrixUser: MatrixUser?,
|
matrixUser: MatrixUser,
|
||||||
showAvatarIndicator: Boolean,
|
showAvatarIndicator: Boolean,
|
||||||
areSearchResultsDisplayed: Boolean,
|
areSearchResultsDisplayed: Boolean,
|
||||||
onToggleSearch: () -> Unit,
|
onToggleSearch: () -> Unit,
|
||||||
|
|
@ -117,7 +117,7 @@ fun RoomListTopBar(
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun DefaultRoomListTopBar(
|
private fun DefaultRoomListTopBar(
|
||||||
matrixUser: MatrixUser?,
|
matrixUser: MatrixUser,
|
||||||
showAvatarIndicator: Boolean,
|
showAvatarIndicator: Boolean,
|
||||||
areSearchResultsDisplayed: Boolean,
|
areSearchResultsDisplayed: Boolean,
|
||||||
scrollBehavior: TopAppBarScrollBehavior,
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
|
@ -142,7 +142,7 @@ private fun DefaultRoomListTopBar(
|
||||||
|
|
||||||
val avatarData by remember(matrixUser) {
|
val avatarData by remember(matrixUser) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
matrixUser?.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
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.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_AVATAR_URL
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
|
@ -93,17 +94,24 @@ class RoomListPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - should start with no user and then load user with success`() = runTest {
|
fun `present - should start with no user and then load user with success`() = runTest {
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
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) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.matrixUser).isNull()
|
assertThat(initialState.matrixUser).isEqualTo(MatrixUser(A_USER_ID))
|
||||||
val withUserState = awaitItem()
|
val withUserState = awaitItem()
|
||||||
assertThat(withUserState.matrixUser).isNotNull()
|
assertThat(withUserState.matrixUser.userId).isEqualTo(A_USER_ID)
|
||||||
assertThat(withUserState.matrixUser!!.userId).isEqualTo(A_USER_ID)
|
assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME)
|
||||||
assertThat(withUserState.matrixUser!!.displayName).isEqualTo(A_USER_NAME)
|
assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||||
assertThat(withUserState.matrixUser!!.avatarUrl).isEqualTo(AN_AVATAR_URL)
|
|
||||||
assertThat(withUserState.showAvatarIndicator).isTrue()
|
assertThat(withUserState.showAvatarIndicator).isTrue()
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +136,6 @@ class RoomListPresenterTests {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.showAvatarIndicator).isTrue()
|
assertThat(initialState.showAvatarIndicator).isTrue()
|
||||||
sessionVerificationService.givenCanVerifySession(false)
|
sessionVerificationService.givenCanVerifySession(false)
|
||||||
assertThat(awaitItem().showAvatarIndicator).isTrue()
|
|
||||||
encryptionService.emitBackupState(BackupState.ENABLED)
|
encryptionService.emitBackupState(BackupState.ENABLED)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.showAvatarIndicator).isFalse()
|
assertThat(finalState.showAvatarIndicator).isFalse()
|
||||||
|
|
@ -139,19 +146,18 @@ class RoomListPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - should start with no user and then load user with error`() = runTest {
|
fun `present - should start with no user and then load user with error`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
userDisplayName = Result.failure(AN_EXCEPTION),
|
userDisplayName = null,
|
||||||
userAvatarUrl = Result.failure(AN_EXCEPTION),
|
userAvatarUrl = null,
|
||||||
)
|
)
|
||||||
|
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION))
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope)
|
val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.matrixUser).isNull()
|
assertThat(initialState.matrixUser).isEqualTo(MatrixUser(matrixClient.sessionId))
|
||||||
val withUserState = awaitItem()
|
// No new state is coming
|
||||||
assertThat(withUserState.matrixUser).isNotNull()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -364,7 +370,6 @@ class RoomListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val summary = createRoomListRoomSummary()
|
val summary = createRoomListRoomSummary()
|
||||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
||||||
|
|
@ -414,8 +419,6 @@ class RoomListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
|
||||||
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val summary = createRoomListRoomSummary()
|
val summary = createRoomListRoomSummary()
|
||||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
||||||
|
|
@ -473,7 +476,6 @@ class RoomListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
eventRecorder.assertEmpty()
|
eventRecorder.assertEmpty()
|
||||||
initialState.eventSink(RoomListEvents.ToggleSearchResults)
|
initialState.eventSink(RoomListEvents.ToggleSearchResults)
|
||||||
|
|
@ -558,7 +560,6 @@ class RoomListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
// The migration screen is shown if the migration screen has not been shown before
|
// The migration screen is shown if the migration screen has not been shown before
|
||||||
assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Migration::class.java)
|
assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Migration::class.java)
|
||||||
|
|
@ -585,7 +586,6 @@ class RoomListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
|
||||||
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java)
|
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import java.io.Closeable
|
||||||
interface MatrixClient : Closeable {
|
interface MatrixClient : Closeable {
|
||||||
val sessionId: SessionId
|
val sessionId: SessionId
|
||||||
val deviceId: String
|
val deviceId: String
|
||||||
|
val userProfile: StateFlow<MatrixUser>
|
||||||
val roomListService: RoomListService
|
val roomListService: RoomListService
|
||||||
val mediaLoader: MatrixMediaLoader
|
val mediaLoader: MatrixMediaLoader
|
||||||
val sessionCoroutineScope: CoroutineScope
|
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.
|
* @param ignoreSdkError if true, the SDK will ignore any error and delete the session data anyway.
|
||||||
*/
|
*/
|
||||||
suspend fun logout(ignoreSdkError: Boolean): String?
|
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 getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
|
||||||
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
|
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
|
||||||
fun roomMembershipObserver(): RoomMembershipObserver
|
fun roomMembershipObserver(): RoomMembershipObserver
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -73,7 +73,9 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -249,6 +251,17 @@ class RustMatrixClient(
|
||||||
|
|
||||||
private val clientDelegateTaskHandle: TaskHandle? = client.setDelegate(clientDelegate)
|
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>> {
|
override val ignoredUsersFlow = mxCallbackFlow<ImmutableList<UserId>> {
|
||||||
client.subscribeToIgnoredUsers(object : IgnoredUsersListener {
|
client.subscribeToIgnoredUsers(object : IgnoredUsersListener {
|
||||||
override fun call(ignoredUserIds: List<String>) {
|
override fun call(ignoredUserIds: List<String>) {
|
||||||
|
|
@ -265,6 +278,10 @@ class RustMatrixClient(
|
||||||
setupVerificationControllerIfNeeded()
|
setupVerificationControllerIfNeeded()
|
||||||
}
|
}
|
||||||
}.launchIn(sessionCoroutineScope)
|
}.launchIn(sessionCoroutineScope)
|
||||||
|
sessionCoroutineScope.launch {
|
||||||
|
// Force a refresh of the profile
|
||||||
|
getUserProfile()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRoom(roomId: RoomId): MatrixRoom? = withContext(sessionDispatcher) {
|
override suspend fun getRoom(roomId: RoomId): MatrixRoom? = withContext(sessionDispatcher) {
|
||||||
|
|
@ -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> =
|
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> =
|
||||||
withContext(sessionDispatcher) {
|
withContext(sessionDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|
@ -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) {
|
override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String> = withContext(sessionDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
client.uploadMedia(mimeType, data, progressCallback?.toProgressWatcher())
|
client.uploadMedia(mimeType, data, progressCallback?.toProgressWatcher())
|
||||||
|
|
|
||||||
|
|
@ -48,14 +48,15 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
||||||
class FakeMatrixClient(
|
class FakeMatrixClient(
|
||||||
override val sessionId: SessionId = A_SESSION_ID,
|
override val sessionId: SessionId = A_SESSION_ID,
|
||||||
override val deviceId: String = "A_DEVICE_ID",
|
override val deviceId: String = "A_DEVICE_ID",
|
||||||
override val sessionCoroutineScope: CoroutineScope = TestScope(),
|
override val sessionCoroutineScope: CoroutineScope = TestScope(),
|
||||||
private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
|
private val userDisplayName: String? = A_USER_NAME,
|
||||||
private val userAvatarUrl: Result<String> = Result.success(AN_AVATAR_URL),
|
private val userAvatarUrl: String? = AN_AVATAR_URL,
|
||||||
override val roomListService: RoomListService = FakeRoomListService(),
|
override val roomListService: RoomListService = FakeRoomListService(),
|
||||||
override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(),
|
override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(),
|
||||||
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
||||||
|
|
@ -73,6 +74,8 @@ class FakeMatrixClient(
|
||||||
var removeAvatarCalled: Boolean = false
|
var removeAvatarCalled: Boolean = false
|
||||||
private set
|
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())
|
override val ignoredUsersFlow: MutableStateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf())
|
||||||
|
|
||||||
private var ignoreUserResult: Result<Unit> = Result.success(Unit)
|
private var ignoreUserResult: Result<Unit> = Result.success(Unit)
|
||||||
|
|
@ -140,12 +143,10 @@ class FakeMatrixClient(
|
||||||
|
|
||||||
override fun close() = Unit
|
override fun close() = Unit
|
||||||
|
|
||||||
override suspend fun loadUserDisplayName(): Result<String> {
|
override suspend fun getUserProfile(): Result<MatrixUser> = simulateLongTask {
|
||||||
return userDisplayName
|
val result = getProfileResults[sessionId]?.getOrNull() ?: MatrixUser(sessionId, userDisplayName, userAvatarUrl)
|
||||||
}
|
_userProfile.tryEmit(result)
|
||||||
|
return Result.success(result)
|
||||||
override suspend fun loadUserAvatarUrl(): Result<String?> {
|
|
||||||
return userAvatarUrl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> {
|
override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> {
|
||||||
|
|
|
||||||
|
|
@ -292,23 +292,30 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||||
eventsForSessions.forEach { (sessionId, notifiableEvents) ->
|
eventsForSessions.forEach { (sessionId, notifiableEvents) ->
|
||||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow()
|
val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow()
|
||||||
val imageLoader = imageLoaderHolder.get(client)
|
val imageLoader = imageLoaderHolder.get(client)
|
||||||
val currentUser = tryOrNull(
|
val userFromCache = client.userProfile.value
|
||||||
onError = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") },
|
val currentUser = if (userFromCache.avatarUrl != null && userFromCache.displayName.isNullOrEmpty().not()) {
|
||||||
operation = {
|
// We have an avatar and a display name, use it
|
||||||
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
|
userFromCache
|
||||||
val myUserDisplayName = client.loadUserDisplayName().getOrNull() ?: sessionId.value
|
} else {
|
||||||
val userAvatarUrl = client.loadUserAvatarUrl().getOrNull()
|
tryOrNull(
|
||||||
MatrixUser(
|
onError = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") },
|
||||||
userId = sessionId,
|
operation = {
|
||||||
displayName = myUserDisplayName,
|
client.getUserProfile().getOrNull()
|
||||||
avatarUrl = userAvatarUrl
|
?.let {
|
||||||
)
|
// displayName cannot be empty else NotificationCompat.MessagingStyle() will crash
|
||||||
}
|
if (it.displayName.isNullOrEmpty()) {
|
||||||
) ?: MatrixUser(
|
it.copy(displayName = sessionId.value)
|
||||||
userId = sessionId,
|
} else {
|
||||||
displayName = sessionId.value,
|
it
|
||||||
avatarUrl = null
|
}
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
) ?: MatrixUser(
|
||||||
|
userId = sessionId,
|
||||||
|
displayName = sessionId.value,
|
||||||
|
avatarUrl = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents, imageLoader)
|
notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents, imageLoader)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue