Update SDK version to 25.03.13 and fix breaking changes (#4406)

Breaking changes addressed:
* Make `MatrixClient.getNotificationSettings()` async, cache its result.
* Use `RoomInfo` for accessing the updated room's info.
* Refactor `MatrixRoom` so it always receives an initial `MatrixRoomInfo` value: this value will be used to make `MatrixRoom.roomInfoFlow` a `StateFlow` so we can assume the initial updated Room data will be present.
* Fetch encryption state when loading a room if it's unknown
This commit is contained in:
Jorge Martin Espinosa 2025-03-19 12:52:57 +01:00 committed by GitHub
parent da9b751847
commit 1659572950
76 changed files with 647 additions and 431 deletions

View file

@ -15,7 +15,7 @@ import io.element.android.x.di.AppComponent
import io.element.android.x.di.DaggerAppComponent import io.element.android.x.di.DaggerAppComponent
import io.element.android.x.info.logApplicationInfo import io.element.android.x.info.logApplicationInfo
import io.element.android.x.initializer.CrashInitializer import io.element.android.x.initializer.CrashInitializer
import io.element.android.x.initializer.TracingInitializer import io.element.android.x.initializer.PlatformInitializer
class ElementXApplication : Application(), DaggerComponentOwner { class ElementXApplication : Application(), DaggerComponentOwner {
override val daggerComponent: AppComponent = DaggerAppComponent.factory().create(this) override val daggerComponent: AppComponent = DaggerAppComponent.factory().create(this)
@ -24,7 +24,7 @@ class ElementXApplication : Application(), DaggerComponentOwner {
super.onCreate() super.onCreate()
AppInitializer.getInstance(this).apply { AppInitializer.getInstance(this).apply {
initializeComponent(CrashInitializer::class.java) initializeComponent(CrashInitializer::class.java)
initializeComponent(TracingInitializer::class.java) initializeComponent(PlatformInitializer::class.java)
initializeComponent(CacheCleanerInitializer::class.java) initializeComponent(CacheCleanerInitializer::class.java)
} }
logApplicationInfo(this) logApplicationInfo(this)

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.platform.InitPlatformService
import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.matrix.api.tracing.TracingService
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@ -27,6 +28,8 @@ interface AppBindings {
fun tracingService(): TracingService fun tracingService(): TracingService
fun platformService(): InitPlatformService
fun bugReporter(): BugReporter fun bugReporter(): BugReporter
fun lockScreenService(): LockScreenService fun lockScreenService(): LockScreenService

View file

@ -22,10 +22,11 @@ import timber.log.Timber
private const val ELEMENT_X_TARGET = "elementx" private const val ELEMENT_X_TARGET = "elementx"
class TracingInitializer : Initializer<Unit> { class PlatformInitializer : Initializer<Unit> {
override fun create(context: Context) { override fun create(context: Context) {
val appBindings = context.bindings<AppBindings>() val appBindings = context.bindings<AppBindings>()
val tracingService = appBindings.tracingService() val tracingService = appBindings.tracingService()
val platformService = appBindings.platformService()
val bugReporter = appBindings.bugReporter() val bugReporter = appBindings.bugReporter()
Timber.plant(tracingService.createTimberTree(ELEMENT_X_TARGET)) Timber.plant(tracingService.createTimberTree(ELEMENT_X_TARGET))
val preferencesStore = appBindings.preferencesStore() val preferencesStore = appBindings.preferencesStore()
@ -38,7 +39,7 @@ class TracingInitializer : Initializer<Unit> {
extraTargets = listOf(ELEMENT_X_TARGET), extraTargets = listOf(ELEMENT_X_TARGET),
) )
bugReporter.setCurrentTracingLogLevel(logLevel.name) bugReporter.setCurrentTracingLogLevel(logLevel.name)
tracingService.setupTracing(tracingConfiguration) platformService.init(tracingConfiguration)
// Also set env variable for rust back trace // Also set env variable for rust back trace
Os.setenv("RUST_BACKTRACE", "1", true) Os.setenv("RUST_BACKTRACE", "1", true)
} }

View file

@ -38,7 +38,8 @@ class DefaultCallWidgetProvider @Inject constructor(
val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
?: elementCallBaseUrlProvider.provides(matrixClient) ?: elementCallBaseUrlProvider.provides(matrixClient)
?: ElementCallConfig.DEFAULT_BASE_URL ?: ElementCallConfig.DEFAULT_BASE_URL
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = room.isEncrypted) val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = isEncrypted)
val callUrl = room.generateWidgetWebViewUrl( val callUrl = room.generateWidgetWebViewUrl(
widgetSettings = widgetSettings, widgetSettings = widgetSettings,
clientId = clientId, clientId = clientId,

View file

@ -23,6 +23,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.isDm
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -74,10 +75,11 @@ private suspend fun showLeaveRoomAlert(
confirmation: MutableState<LeaveRoomState.Confirmation>, confirmation: MutableState<LeaveRoomState.Confirmation>,
) { ) {
matrixClient.getRoom(roomId)?.use { room -> matrixClient.getRoom(roomId)?.use { room ->
val roomInfo = room.roomInfoFlow.first()
confirmation.value = when { confirmation.value = when {
room.isDm -> Dm(roomId) roomInfo.isDm -> Dm(roomId)
!room.isPublic -> PrivateRoom(roomId) !roomInfo.isPublic -> PrivateRoom(roomId)
room.joinedMemberCount == 1L -> LastUserInRoom(roomId) roomInfo.joinedMembersCount == 1L -> LastUserInRoom(roomId)
else -> Generic(roomId) else -> Generic(roomId)
} }
} }

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -52,7 +53,9 @@ class LeaveRoomPresenterTest {
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
result = FakeMatrixRoom() result = FakeMatrixRoom().apply {
givenRoomInfo(aRoomInfo(isDirect = false, isPublic = true, joinedMembersCount = 10))
}
) )
} }
) )
@ -72,7 +75,9 @@ class LeaveRoomPresenterTest {
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
result = FakeMatrixRoom(isPublic = false), result = FakeMatrixRoom().apply {
givenRoomInfo(aRoomInfo(isPublic = false))
},
) )
} }
) )
@ -92,7 +97,9 @@ class LeaveRoomPresenterTest {
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
result = FakeMatrixRoom(joinedMemberCount = 1), result = FakeMatrixRoom().apply {
givenRoomInfo(aRoomInfo(joinedMembersCount = 1))
},
) )
} }
) )
@ -112,7 +119,9 @@ class LeaveRoomPresenterTest {
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
result = FakeMatrixRoom(activeMemberCount = 2, isDirect = true), result = FakeMatrixRoom().apply {
givenRoomInfo(aRoomInfo(isDirect = true, activeMembersCount = 2))
},
) )
} }
) )

View file

@ -93,7 +93,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!! val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!!
if (matrixHomeServerDetails.supportsOidcLogin) { if (matrixHomeServerDetails.supportsOidcLogin) {
// Retrieve the details right now // Retrieve the details right now
val oidcPrompt = if (params.isAccountCreation) OidcPrompt.Create else OidcPrompt.Consent val oidcPrompt = if (params.isAccountCreation) OidcPrompt.Create else OidcPrompt.Login
LoginFlow.OidcFlow(authenticationService.getOidcUrl(oidcPrompt).getOrThrow()) LoginFlow.OidcFlow(authenticationService.getOidcUrl(oidcPrompt).getOrThrow())
} else if (params.isAccountCreation) { } else if (params.isAccountCreation) {
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl) val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)

View file

@ -59,11 +59,14 @@ import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugIn
import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.libraries.mediaplayer.api.MediaPlayer
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ContributesNode(RoomScope::class) @ContributesNode(RoomScope::class)
class MessagesNode @AssistedInject constructor( class MessagesNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val coroutineScope: CoroutineScope,
private val room: MatrixRoom, private val room: MatrixRoom,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
messageComposerPresenterFactory: MessageComposerPresenter.Factory, messageComposerPresenterFactory: MessageComposerPresenter.Factory,
@ -108,7 +111,7 @@ class MessagesNode @AssistedInject constructor(
super.onBuilt() super.onBuilt()
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { onCreate = {
analyticsService.capture(room.toAnalyticsViewRoom()) coroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) }
}, },
onDestroy = { onDestroy = {
mediaPlayer.close() mediaPlayer.close()

View file

@ -128,7 +128,7 @@ class MessagesPresenter @AssistedInject constructor(
override fun present(): MessagesState { override fun present(): MessagesState {
htmlConverterProvider.Update(currentUserId = room.sessionId) htmlConverterProvider.Update(currentUserId = room.sessionId)
val roomInfo by room.roomInfoFlow.collectAsState(null) val roomInfo by room.roomInfoFlow.collectAsState()
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val composerState = composerPresenter.present() val composerState = composerPresenter.present()
val voiceMessageComposerState = voiceMessageComposerPresenter.present() val voiceMessageComposerState = voiceMessageComposerPresenter.present()
@ -147,13 +147,13 @@ class MessagesPresenter @AssistedInject constructor(
val userEventPermissions by userEventPermissions(syncUpdateFlow.value) val userEventPermissions by userEventPermissions(syncUpdateFlow.value)
val roomName: AsyncData<String> by remember { val roomName: AsyncData<String> by remember {
derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized } derivedStateOf { roomInfo.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
} }
val roomAvatar: AsyncData<AvatarData> by remember { val roomAvatar: AsyncData<AvatarData> by remember {
derivedStateOf { roomInfo?.avatarData()?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized } derivedStateOf { AsyncData.Success(roomInfo.avatarData()) }
} }
val heroes by remember { val heroes by remember {
derivedStateOf { roomInfo?.heroes().orEmpty().toPersistentList() } derivedStateOf { roomInfo.heroes().toPersistentList() }
} }
var hasDismissedInviteDialog by rememberSaveable { var hasDismissedInviteDialog by rememberSaveable {
@ -164,14 +164,20 @@ class MessagesPresenter @AssistedInject constructor(
// as those will be handled by the timeline. // as those will be handled by the timeline.
withContext(dispatchers.io) { withContext(dispatchers.io) {
room.setUnreadFlag(isUnread = false) room.setUnreadFlag(isUnread = false)
// If for some reason the encryption state is unknown, fetch it
if (roomInfo.isEncrypted == null) {
room.getUpdatedIsEncrypted()
}
} }
} }
val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) } val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) }
var showReinvitePrompt by remember { mutableStateOf(false) } var showReinvitePrompt by remember { mutableStateOf(false) }
LaunchedEffect(hasDismissedInviteDialog, composerState.textEditorState.hasFocus(), syncUpdateFlow.value) { val composerHasFocus by remember { derivedStateOf { composerState.textEditorState.hasFocus() } }
LaunchedEffect(hasDismissedInviteDialog, composerHasFocus, roomInfo) {
withContext(dispatchers.io) { withContext(dispatchers.io) {
showReinvitePrompt = !hasDismissedInviteDialog && composerState.textEditorState.hasFocus() && room.isDm && room.activeMemberCount == 1L showReinvitePrompt = !hasDismissedInviteDialog && composerHasFocus && roomInfo.isDm && roomInfo.activeMembersCount == 1L
} }
} }
val isOnline by syncService.isOnline().collectAsState() val isOnline by syncService.isOnline().collectAsState()
@ -189,9 +195,8 @@ class MessagesPresenter @AssistedInject constructor(
val dmRoomMember by room.getDirectRoomMember(membersState) val dmRoomMember by room.getDirectRoomMember(membersState)
val roomMemberIdentityStateChanges = identityChangeState.roomMemberIdentityStateChanges val roomMemberIdentityStateChanges = identityChangeState.roomMemberIdentityStateChanges
// TODO use `RoomInfo.isEncrypted` as a key here once it's available LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) {
LifecycleResumeEffect(dmRoomMember, roomMemberIdentityStateChanges) { if (roomInfo.isEncrypted == true) {
if (room.isEncrypted) {
val dmRoomMemberId = dmRoomMember?.userId val dmRoomMemberId = dmRoomMember?.userId
localCoroutineScope.launch { localCoroutineScope.launch {
dmRoomMemberId?.let { userId -> dmRoomMemberId?.let { userId ->
@ -275,7 +280,7 @@ class MessagesPresenter @AssistedInject constructor(
return AvatarData( return AvatarData(
id = id.value, id = id.value,
name = name, name = name,
url = avatarUrl ?: room.avatarUrl, url = avatarUrl ?: room.info().avatarUrl,
size = AvatarSize.TimelineRoom size = AvatarSize.TimelineRoom
) )
} }

View file

@ -202,7 +202,7 @@ class MessageComposerPresenter @AssistedInject constructor(
suspend fun canSendRoomMention(): Boolean { suspend fun canSendRoomMention(): Boolean {
val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false) val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false)
return !room.isDm && userCanSendAtRoom return !room.isDm() && userCanSendAtRoom
} }
// This will trigger a search immediately when `@` is typed // This will trigger a search immediately when `@` is typed

View file

@ -38,11 +38,11 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
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.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.ui.room.isDmAsState
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
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
@ -85,10 +85,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): PinnedMessagesListState { override fun present(): PinnedMessagesListState {
val timelineRoomInfo = remember { val isDm by room.isDmAsState()
val timelineRoomInfo = remember(isDm) {
TimelineRoomInfo( TimelineRoomInfo(
isDm = room.isDm, isDm = isDm,
name = room.displayName, name = room.info().name,
// We don't need to compute those values // We don't need to compute those values
userHasPermissionToSendMessage = false, userHasPermissionToSendMessage = false,
userHasPermissionToSendReaction = false, userHasPermissionToSendReaction = false,

View file

@ -34,6 +34,7 @@ import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.features.poll.api.actions.SendPollResponseAction
import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UniqueId
@ -95,7 +96,7 @@ class TimelinePresenter @AssistedInject constructor(
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) } val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
val roomInfo by room.roomInfoFlow.collectAsState(initial = null) val roomInfo by room.roomInfoFlow.collectAsState()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
@ -231,15 +232,15 @@ class TimelinePresenter @AssistedInject constructor(
val typingNotificationState = typingNotificationPresenter.present() val typingNotificationState = typingNotificationPresenter.present()
val roomCallState = roomCallStatePresenter.present() val roomCallState = roomCallStatePresenter.present()
val timelineRoomInfo by remember(typingNotificationState, roomCallState) { val timelineRoomInfo by remember(typingNotificationState, roomCallState, roomInfo) {
derivedStateOf { derivedStateOf {
TimelineRoomInfo( TimelineRoomInfo(
name = room.displayName, name = roomInfo.name,
isDm = room.isDm, isDm = roomInfo.isDm.orFalse(),
userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = userHasPermissionToSendReaction, userHasPermissionToSendReaction = userHasPermissionToSendReaction,
roomCallState = roomCallState, roomCallState = roomCallState,
pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(), pinnedEventIds = roomInfo.pinnedEventIds.orEmpty(),
typingNotificationState = typingNotificationState, typingNotificationState = typingNotificationState,
) )
} }

View file

@ -536,15 +536,15 @@ class MessagesPresenterTest {
fun `present - shows prompt to reinvite users in DM`() = runTest { fun `present - shows prompt to reinvite users in DM`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
isDirect = true,
activeMemberCount = 1L,
canUserSendMessageResult = { _, _ -> Result.success(true) }, canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) }, canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) }, typingNoticeResult = { Result.success(Unit) },
canUserPinUnpinResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) },
) ).apply {
givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 1, activeMembersCount = 1))
}
val presenter = createMessagesPresenter(matrixRoom = room) val presenter = createMessagesPresenter(matrixRoom = room)
presenter.testWithLifecycleOwner { presenter.testWithLifecycleOwner {
val initialState = awaitItem() val initialState = awaitItem()
@ -552,7 +552,8 @@ class MessagesPresenterTest {
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
// When the input field is focused we show the alert // When the input field is focused we show the alert
(initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
skipItems(1) // Skip intermediate states
skipItems(2)
val focusedState = awaitItem() val focusedState = awaitItem()
assertThat(focusedState.showReinvitePrompt).isTrue() assertThat(focusedState.showReinvitePrompt).isTrue()
// If it's dismissed then we stop showing the alert // If it's dismissed then we stop showing the alert
@ -567,20 +568,22 @@ class MessagesPresenterTest {
fun `present - doesn't show reinvite prompt in non-direct room`() = runTest { fun `present - doesn't show reinvite prompt in non-direct room`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
isDirect = false,
activeMemberCount = 1L,
canUserSendMessageResult = { _, _ -> Result.success(true) }, canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) }, canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) }, typingNoticeResult = { Result.success(Unit) },
canUserPinUnpinResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) },
) ).apply {
givenRoomInfo(aRoomInfo(isDirect = false, joinedMembersCount = 1, activeMembersCount = 1))
}
val presenter = createMessagesPresenter(matrixRoom = room) val presenter = createMessagesPresenter(matrixRoom = room)
presenter.testWithLifecycleOwner { presenter.testWithLifecycleOwner {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
(initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
// Skip intermediate events
skipItems(1)
val focusedState = awaitItem() val focusedState = awaitItem()
assertThat(focusedState.showReinvitePrompt).isFalse() assertThat(focusedState.showReinvitePrompt).isFalse()
} }
@ -590,20 +593,22 @@ class MessagesPresenterTest {
fun `present - doesn't show reinvite prompt if other party is present`() = runTest { fun `present - doesn't show reinvite prompt if other party is present`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
isDirect = true,
activeMemberCount = 2L,
canUserSendMessageResult = { _, _ -> Result.success(true) }, canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) }, canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) }, typingNoticeResult = { Result.success(Unit) },
canUserPinUnpinResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) },
) ).apply {
givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 2, activeMembersCount = 2))
}
val presenter = createMessagesPresenter(matrixRoom = room) val presenter = createMessagesPresenter(matrixRoom = room)
presenter.testWithLifecycleOwner { presenter.testWithLifecycleOwner {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
(initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
// Skip intermediate events
skipItems(1)
val focusedState = awaitItem() val focusedState = awaitItem()
assertThat(focusedState.showReinvitePrompt).isFalse() assertThat(focusedState.showReinvitePrompt).isFalse()
} }
@ -1090,28 +1095,25 @@ class MessagesPresenterTest {
fun `present - when room is encrypted and a DM, the DM user's identity state is fetched onResume`() = runTest { fun `present - when room is encrypted and a DM, the DM user's identity state is fetched onResume`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
isEncrypted = true,
isDirect = true,
activeMemberCount = 2L,
canUserSendMessageResult = { _, _ -> Result.success(true) }, canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) }, canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) }, typingNoticeResult = { Result.success(Unit) },
canUserPinUnpinResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) },
initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true)
).apply { ).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2)))) givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2))))
givenRoomInfo(aRoomInfo(id = roomId, name = "", isDirect = true))
} }
val encryptionService = FakeEncryptionService(getUserIdentityResult = { Result.success(IdentityState.Verified) }) val encryptionService = FakeEncryptionService(getUserIdentityResult = { Result.success(IdentityState.Verified) })
val presenter = createMessagesPresenter(matrixRoom = room, encryptionService = encryptionService) val presenter = createMessagesPresenter(matrixRoom = room, encryptionService = encryptionService)
val lifecycleOwner = FakeLifecycleOwner() val lifecycleOwner = FakeLifecycleOwner()
presenter.testWithLifecycleOwner(lifecycleOwner) { presenter.testWithLifecycleOwner(lifecycleOwner) {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.dmUserVerificationState).isNull() assertThat(initialState.dmUserVerificationState).isNull()
skipItems(1)
ensureAllEventsConsumed() ensureAllEventsConsumed()
lifecycleOwner.givenState(Lifecycle.State.RESUMED) lifecycleOwner.givenState(Lifecycle.State.RESUMED)

View file

@ -19,6 +19,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -44,7 +45,9 @@ class IdentityChangeStatePresenterTest {
@Test @Test
fun `present - when the room emits identity change, the presenter emits new state`() = runTest { fun `present - when the room emits identity change, the presenter emits new state`() = runTest {
val room = FakeMatrixRoom(isEncrypted = true) val room = FakeMatrixRoom().apply {
givenRoomInfo(aRoomInfo(isEncrypted = true))
}
val presenter = createIdentityChangeStatePresenter(room) val presenter = createIdentityChangeStatePresenter(room)
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
@ -67,10 +70,7 @@ class IdentityChangeStatePresenterTest {
@Test @Test
fun `present - when the clear room emits identity change, the presenter does not emit new state`() = runTest { fun `present - when the clear room emits identity change, the presenter does not emit new state`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(enableEncryptionResult = { Result.success(Unit) })
isEncrypted = false,
enableEncryptionResult = { Result.success(Unit) }
)
val presenter = createIdentityChangeStatePresenter(room) val presenter = createIdentityChangeStatePresenter(room)
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
@ -85,8 +85,9 @@ class IdentityChangeStatePresenterTest {
) )
// No item emitted. // No item emitted.
expectNoEvents() expectNoEvents()
// Room become encrypted. // Room becomes encrypted.
room.enableEncryption() room.givenRoomInfo(aRoomInfo(isEncrypted = true))
val finalItem = awaitItem() val finalItem = awaitItem()
assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1) assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1)
val value = finalItem.roomMemberIdentityStateChanges.first() val value = finalItem.roomMemberIdentityStateChanges.first()
@ -99,7 +100,7 @@ class IdentityChangeStatePresenterTest {
@Test @Test
fun `present - when the room emits identity change, the presenter emits new state with member details`() = fun `present - when the room emits identity change, the presenter emits new state with member details`() =
runTest { runTest {
val room = FakeMatrixRoom(isEncrypted = true).apply { val room = FakeMatrixRoom().apply {
givenRoomMembersState( givenRoomMembersState(
MatrixRoomMembersState.Ready( MatrixRoomMembersState.Ready(
listOf( listOf(
@ -110,6 +111,7 @@ class IdentityChangeStatePresenterTest {
).toImmutableList() ).toImmutableList()
) )
) )
givenRoomInfo(aRoomInfo(isEncrypted = true))
} }
val presenter = createIdentityChangeStatePresenter(room) val presenter = createIdentityChangeStatePresenter(room)
presenter.test { presenter.test {

View file

@ -63,6 +63,7 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
@ -998,7 +999,6 @@ class MessageComposerPresenterTest {
val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN) val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN)
var canUserTriggerRoomNotificationResult = true var canUserTriggerRoomNotificationResult = true
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
isDirect = false,
canUserTriggerRoomNotificationResult = { Result.success(canUserTriggerRoomNotificationResult) }, canUserTriggerRoomNotificationResult = { Result.success(canUserTriggerRoomNotificationResult) },
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
).apply { ).apply {
@ -1007,6 +1007,7 @@ class MessageComposerPresenterTest {
persistentListOf(currentUser, invitedUser, bob, david), persistentListOf(currentUser, invitedUser, bob, david),
) )
) )
givenRoomInfo(aRoomInfo(isDirect = false))
} }
val flagsService = FakeFeatureFlagService( val flagsService = FakeFeatureFlagService(
mapOf( mapOf(
@ -1060,9 +1061,6 @@ class MessageComposerPresenterTest {
val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN) val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN)
val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN) val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN)
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
isDirect = true,
activeMemberCount = 2,
isEncrypted = true,
canUserTriggerRoomNotificationResult = { Result.success(true) }, canUserTriggerRoomNotificationResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
).apply { ).apply {
@ -1071,6 +1069,12 @@ class MessageComposerPresenterTest {
persistentListOf(currentUser, invitedUser, bob, david), persistentListOf(currentUser, invitedUser, bob, david),
) )
) )
givenRoomInfo(
aRoomInfo(
isDirect = true,
activeMembersCount = 2,
)
)
} }
val flagsService = FakeFeatureFlagService( val flagsService = FakeFeatureFlagService(
mapOf( mapOf(

View file

@ -26,12 +26,12 @@ class RoomCallStatePresenter @Inject constructor(
) : Presenter<RoomCallState> { ) : Presenter<RoomCallState> {
@Composable @Composable
override fun present(): RoomCallState { override fun present(): RoomCallState {
val roomInfo by room.roomInfoFlow.collectAsState(null) val roomInfo by room.roomInfoFlow.collectAsState()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value) val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value)
val isUserInTheCall by remember { val isUserInTheCall by remember {
derivedStateOf { derivedStateOf {
room.sessionId in roomInfo?.activeRoomCallParticipants.orEmpty() room.sessionId in roomInfo.activeRoomCallParticipants
} }
} }
val currentCall by currentCallService.currentCall.collectAsState() val currentCall by currentCallService.currentCall.collectAsState()
@ -41,7 +41,7 @@ class RoomCallStatePresenter @Inject constructor(
} }
} }
val callState = when { val callState = when {
roomInfo?.hasRoomCall == true -> RoomCallState.OnGoing( roomInfo.hasRoomCall -> RoomCallState.OnGoing(
canJoinCall = canJoinCall, canJoinCall = canJoinCall,
isUserInTheCall = isUserInTheCall, isUserInTheCall = isUserInTheCall,
isUserLocallyInTheCall = isUserLocallyInTheCall, isUserLocallyInTheCall = isUserLocallyInTheCall,

View file

@ -58,12 +58,10 @@ class RoomCallStatePresenterTest {
fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest { fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
canUserJoinCallResult = { Result.success(false) }, canUserJoinCallResult = { Result.success(false) },
).apply { initialRoomInfo = aRoomInfo(hasRoomCall = true),
givenRoomInfo(aRoomInfo(hasRoomCall = true)) )
}
val presenter = createRoomCallStatePresenter(matrixRoom = room) val presenter = createRoomCallStatePresenter(matrixRoom = room)
presenter.test { presenter.test {
skipItems(1)
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = false, canJoinCall = false,

View file

@ -25,7 +25,6 @@ import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.features.roomdetails.impl.securityandprivacy.permissions.securityAndPrivacyPermissionsAsState import io.element.android.features.roomdetails.impl.securityandprivacy.permissions.securityAndPrivacyPermissionsAsState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
@ -36,7 +35,6 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
@ -44,6 +42,7 @@ import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.ui.room.canHandleKnockRequestsAsState import io.element.android.libraries.matrix.ui.room.canHandleKnockRequestsAsState
import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import io.element.android.libraries.matrix.ui.room.isDmAsState
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@ -72,22 +71,22 @@ class RoomDetailsPresenter @Inject constructor(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val leaveRoomState = leaveRoomPresenter.present() val leaveRoomState = leaveRoomPresenter.present()
val canShowNotificationSettings = remember { mutableStateOf(false) } val canShowNotificationSettings = remember { mutableStateOf(false) }
val roomInfo by room.roomInfoFlow.collectAsState(initial = null) val roomInfo by room.roomInfoFlow.collectAsState()
val isUserAdmin = room.isOwnUserAdmin() val isUserAdmin = room.isOwnUserAdmin()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val roomAvatar by remember { derivedStateOf { roomInfo?.avatarUrl ?: room.avatarUrl } } val roomAvatar by remember { derivedStateOf { roomInfo.avatarUrl } }
val roomName by remember { derivedStateOf { (roomInfo?.name ?: room.displayName).trim() } } val roomName by remember { derivedStateOf { roomInfo.name?.trim().orEmpty() } }
val roomTopic by remember { derivedStateOf { roomInfo?.topic ?: room.topic } } val roomTopic by remember { derivedStateOf { roomInfo.topic } }
val isFavorite by remember { derivedStateOf { roomInfo?.isFavorite.orFalse() } } val isFavorite by remember { derivedStateOf { roomInfo.isFavorite } }
val joinRule by remember { derivedStateOf { roomInfo?.joinRule } } val joinRule by remember { derivedStateOf { roomInfo.joinRule } }
val canShowPinnedMessages = isPinnedMessagesFeatureEnabled() val canShowPinnedMessages = isPinnedMessagesFeatureEnabled()
var canShowMediaGallery by remember { mutableStateOf(false) } var canShowMediaGallery by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
canShowMediaGallery = featureFlagService.isFeatureEnabled(FeatureFlags.MediaGallery) canShowMediaGallery = featureFlagService.isFeatureEnabled(FeatureFlags.MediaGallery)
} }
val pinnedMessagesCount by remember { derivedStateOf { roomInfo?.pinnedEventIds?.size } } val pinnedMessagesCount by remember { derivedStateOf { roomInfo.pinnedEventIds.size } }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings)
@ -100,6 +99,9 @@ class RoomDetailsPresenter @Inject constructor(
val membersState by room.membersStateFlow.collectAsState() val membersState by room.membersStateFlow.collectAsState()
val canInvite by getCanInvite(membersState) val canInvite by getCanInvite(membersState)
val canonicalAlias by remember { derivedStateOf { roomInfo.canonicalAlias } }
val isEncrypted by remember { derivedStateOf { roomInfo.isEncrypted == true } }
val isDm by room.isDmAsState()
val canEditName by getCanSendState(membersState, StateEventType.ROOM_NAME) val canEditName by getCanSendState(membersState, StateEventType.ROOM_NAME)
val canEditAvatar by getCanSendState(membersState, StateEventType.ROOM_AVATAR) val canEditAvatar by getCanSendState(membersState, StateEventType.ROOM_AVATAR)
val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC) val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC)
@ -108,6 +110,7 @@ class RoomDetailsPresenter @Inject constructor(
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
val roomType = getRoomType(dmMember, currentMember) val roomType = getRoomType(dmMember, currentMember)
val roomCallState = roomCallStatePresenter.present() val roomCallState = roomCallStatePresenter.present()
val joinedMemberCount by remember { derivedStateOf { roomInfo.joinedMembersCount } }
val topicState = remember(canEditTopic, roomTopic, roomType) { val topicState = remember(canEditTopic, roomTopic, roomType) {
val topic = roomTopic val topic = roomTopic
@ -140,7 +143,7 @@ class RoomDetailsPresenter @Inject constructor(
} }
RoomDetailsEvent.UnmuteNotification -> { RoomDetailsEvent.UnmuteNotification -> {
scope.launch(dispatchers.io) { scope.launch(dispatchers.io) {
client.notificationSettingsService().unmuteRoom(room.roomId, room.isEncrypted, room.isOneToOne) client.notificationSettingsService().unmuteRoom(room.roomId, isEncrypted, room.isOneToOne)
} }
} }
is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite) is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite)
@ -165,11 +168,11 @@ class RoomDetailsPresenter @Inject constructor(
return RoomDetailsState( return RoomDetailsState(
roomId = room.roomId, roomId = room.roomId,
roomName = roomName, roomName = roomName,
roomAlias = room.canonicalAlias, roomAlias = canonicalAlias,
roomAvatarUrl = roomAvatar, roomAvatarUrl = roomAvatar,
roomTopic = topicState, roomTopic = topicState,
memberCount = room.joinedMemberCount, memberCount = joinedMemberCount,
isEncrypted = room.isEncrypted, isEncrypted = isEncrypted,
canInvite = canInvite, canInvite = canInvite,
canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room, canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room,
canShowNotificationSettings = canShowNotificationSettings.value, canShowNotificationSettings = canShowNotificationSettings.value,
@ -179,9 +182,9 @@ class RoomDetailsPresenter @Inject constructor(
leaveRoomState = leaveRoomState, leaveRoomState = leaveRoomState,
roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(),
isFavorite = isFavorite, isFavorite = isFavorite,
displayRolesAndPermissionsSettings = !room.isDm && isUserAdmin, displayRolesAndPermissionsSettings = !isDm && isUserAdmin,
isPublic = joinRule == JoinRule.Public, isPublic = joinRule == JoinRule.Public,
heroes = roomInfo?.heroes.orEmpty().toPersistentList(), heroes = roomInfo.heroes.toPersistentList(),
canShowPinnedMessages = canShowPinnedMessages, canShowPinnedMessages = canShowPinnedMessages,
canShowMediaGallery = canShowMediaGallery, canShowMediaGallery = canShowMediaGallery,
pinnedMessagesCount = pinnedMessagesCount, pinnedMessagesCount = pinnedMessagesCount,

View file

@ -183,7 +183,7 @@ class RoomMemberListPresenter @AssistedInject constructor(
} }
private suspend fun RoomMember.withIdentityState(identityStates: ImmutableMap<UserId, IdentityState>): RoomMemberWithIdentityState { private suspend fun RoomMember.withIdentityState(identityStates: ImmutableMap<UserId, IdentityState>): RoomMemberWithIdentityState {
return if (!room.isEncrypted) { return if (room.info().isEncrypted != true) {
RoomMemberWithIdentityState(this, null) RoomMemberWithIdentityState(this, null)
} else { } else {
val identityState = identityStates[userId] ?: encryptionService.getUserIdentity(userId).getOrNull() val identityState = identityStates[userId] ?: encryptionService.getUserIdentity(userId).getOrNull()

View file

@ -27,7 +27,10 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -47,6 +50,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
private val userProfilePresenter = userProfilePresenterFactory.create(roomMemberId) private val userProfilePresenter = userProfilePresenterFactory.create(roomMemberId)
@OptIn(ExperimentalCoroutinesApi::class)
@Composable @Composable
override fun present(): UserProfileState { override fun present(): UserProfileState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -75,21 +79,22 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
val userProfileState = userProfilePresenter.present() val userProfileState = userProfilePresenter.present()
val identityStateChanges by produceState<IdentityStateChange?>(initialValue = null) { val identityStateChanges by produceState<IdentityStateChange?>(initialValue = null) {
if (room.isEncrypted) { room.roomInfoFlow.filter { it.isEncrypted == true }
// Fetch the initial identity state manually .flatMapLatest {
val identityState = encryptionService.getUserIdentity(roomMemberId).getOrNull() // Fetch the initial identity state manually
value = identityState?.let { IdentityStateChange(roomMemberId, it) } val identityState = encryptionService.getUserIdentity(roomMemberId).getOrNull()
value = identityState?.let { IdentityStateChange(roomMemberId, it) }
// Subscribe to the identity changes // Subscribe to the identity changes
room.roomMemberIdentityStateChange() room.roomMemberIdentityStateChange()
.map { it.find { it.identityRoomMember.userId == roomMemberId } } .map { it.find { it.identityRoomMember.userId == roomMemberId } }
.map { roomMemberIdentityStateChange -> .map { roomMemberIdentityStateChange ->
// If we didn't receive any info, manually fetch it // If we didn't receive any info, manually fetch it
roomMemberIdentityStateChange?.identityState ?: encryptionService.getUserIdentity(roomMemberId).getOrNull() roomMemberIdentityStateChange?.identityState ?: encryptionService.getUserIdentity(roomMemberId).getOrNull()
} }
.filterNotNull() .filterNotNull()
.collect { value = IdentityStateChange(roomMemberId, it) } }
} .collect { value = IdentityStateChange(roomMemberId, it) }
} }
val verificationState = remember(identityStateChanges) { val verificationState = remember(identityStateChanges) {

View file

@ -50,7 +50,7 @@ class RoomMembersModerationPresenter @Inject constructor(
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val canBan by room.canBanAsState(syncUpdateFlow.value) val canBan by room.canBanAsState(syncUpdateFlow.value)
val canKick by room.canKickAsState(syncUpdateFlow.value) val canKick by room.canKickAsState(syncUpdateFlow.value)
val isDm by room.isDmAsState(syncUpdateFlow.value) val isDm by room.isDmAsState()
val currentUserMemberPowerLevel by room.userPowerLevelAsState(syncUpdateFlow.value) val currentUserMemberPowerLevel by room.userPowerLevelAsState(syncUpdateFlow.value)
val canDisplayModerationActions by remember { val canDisplayModerationActions by remember {

View file

@ -12,6 +12,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@ -78,11 +79,23 @@ class RoomNotificationSettingsPresenter @AssistedInject constructor(
mutableStateOf(null) mutableStateOf(null)
} }
val displayName by produceState(room.info().name) {
room.roomInfoFlow.collect { value = it.name }
}
val isRoomEncrypted by produceState(room.info().isEncrypted) {
room.roomInfoFlow.collect { value = it.isEncrypted }
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
getDefaultRoomNotificationMode(defaultRoomNotificationMode) getDefaultRoomNotificationMode(defaultRoomNotificationMode)
fetchNotificationSettings(pendingRoomNotificationMode, roomNotificationSettings) fetchNotificationSettings(pendingRoomNotificationMode, roomNotificationSettings)
observeNotificationSettings(pendingRoomNotificationMode, roomNotificationSettings) observeNotificationSettings(pendingRoomNotificationMode, roomNotificationSettings)
shouldDisplayMentionsOnlyDisclaimer = room.isEncrypted && !notificationSettingsService.canHomeServerPushEncryptedEventsToDevice().getOrDefault(true) }
LaunchedEffect(isRoomEncrypted) {
shouldDisplayMentionsOnlyDisclaimer = isRoomEncrypted == true &&
!notificationSettingsService.canHomeServerPushEncryptedEventsToDevice().getOrDefault(true)
} }
fun handleEvents(event: RoomNotificationSettingsEvents) { fun handleEvents(event: RoomNotificationSettingsEvents) {
@ -113,7 +126,7 @@ class RoomNotificationSettingsPresenter @AssistedInject constructor(
return RoomNotificationSettingsState( return RoomNotificationSettingsState(
showUserDefinedSettingStyle = showUserDefinedSettingStyle, showUserDefinedSettingStyle = showUserDefinedSettingStyle,
roomName = room.displayName, roomName = displayName.orEmpty(),
roomNotificationSettings = roomNotificationSettings.value, roomNotificationSettings = roomNotificationSettings.value,
pendingRoomNotificationMode = pendingRoomNotificationMode.value, pendingRoomNotificationMode = pendingRoomNotificationMode.value,
pendingSetDefault = pendingSetDefault.value, pendingSetDefault = pendingSetDefault.value,
@ -143,16 +156,18 @@ class RoomNotificationSettingsPresenter @AssistedInject constructor(
roomNotificationSettings: MutableState<AsyncData<RoomNotificationSettings>> roomNotificationSettings: MutableState<AsyncData<RoomNotificationSettings>>
) = launch { ) = launch {
suspend { suspend {
val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
pendingModeState.value = null pendingModeState.value = null
notificationSettingsService.getRoomNotificationSettings(room.roomId, room.isEncrypted, room.isOneToOne).getOrThrow() notificationSettingsService.getRoomNotificationSettings(room.roomId, isEncrypted, room.isOneToOne).getOrThrow()
}.runCatchingUpdatingState(roomNotificationSettings) }.runCatchingUpdatingState(roomNotificationSettings)
} }
private fun CoroutineScope.getDefaultRoomNotificationMode( private fun CoroutineScope.getDefaultRoomNotificationMode(
defaultRoomNotificationMode: MutableState<RoomNotificationMode?> defaultRoomNotificationMode: MutableState<RoomNotificationMode?>
) = launch { ) = launch {
val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode( defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode(
room.isEncrypted, isEncrypted,
room.isOneToOne room.isOneToOne
).getOrThrow() ).getOrThrow()
} }

View file

@ -39,7 +39,7 @@ class RolesAndPermissionsPresenter @Inject constructor(
@Composable @Composable
override fun present(): RolesAndPermissionsState { override fun present(): RolesAndPermissionsState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val roomInfo by room.roomInfoFlow.collectAsState(initial = null) val roomInfo by room.roomInfoFlow.collectAsState()
val roomMembers by room.membersStateFlow.collectAsState() val roomMembers by room.membersStateFlow.collectAsState()
// Get the list of active room members (joined or invited), in order to filter members present in the power // Get the list of active room members (joined or invited), in order to filter members present in the power
// level state Event. // level state Event.
@ -109,8 +109,8 @@ class RolesAndPermissionsPresenter @Inject constructor(
} }
} }
private fun MatrixRoomInfo?.userCountWithRole(userIds: List<UserId>, role: RoomMember.Role): Int { private fun MatrixRoomInfo.userCountWithRole(userIds: List<UserId>, role: RoomMember.Role): Int {
return this?.userPowerLevels.orEmpty().count { (userId, level) -> return this.userPowerLevels.count { (userId, level) ->
RoomMember.Role.forPowerLevel(level) == role && userId in userIds RoomMember.Role.forPowerLevel(level) == role && userId in userIds
} }
} }

View file

@ -106,10 +106,10 @@ class ChangeRolesPresenter @AssistedInject constructor(
val hasPendingChanges = usersWithRole.value != selectedUsers.value val hasPendingChanges = usersWithRole.value != selectedUsers.value
val roomInfo by room.roomInfoFlow.collectAsState(initial = null) val roomInfo by room.roomInfoFlow.collectAsState()
fun canChangeMemberRole(userId: UserId): Boolean { fun canChangeMemberRole(userId: UserId): Boolean {
// An admin can't remove or demote another admin // An admin can't remove or demote another admin
val powerLevel = roomInfo?.userPowerLevels?.get(userId) ?: 0L val powerLevel = roomInfo.userPowerLevels[userId] ?: 0L
return RoomMember.Role.forPowerLevel(powerLevel) != RoomMember.Role.ADMIN return RoomMember.Role.forPowerLevel(powerLevel) != RoomMember.Role.ADMIN
} }

View file

@ -57,7 +57,7 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor(
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) } val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val homeserverName = remember { matrixClient.userIdServerName() } val homeserverName = remember { matrixClient.userIdServerName() }
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val roomInfo = room.roomInfoFlow.collectAsState(null) val roomInfo by room.roomInfoFlow.collectAsState()
val savedIsVisibleInRoomDirectory = remember { mutableStateOf<AsyncData<Boolean>>(AsyncData.Uninitialized) } val savedIsVisibleInRoomDirectory = remember { mutableStateOf<AsyncData<Boolean>>(AsyncData.Uninitialized) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -66,12 +66,13 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor(
val savedSettings by remember { val savedSettings by remember {
derivedStateOf { derivedStateOf {
val historyVisibility = roomInfo.historyVisibility.map()
SecurityAndPrivacySettings( SecurityAndPrivacySettings(
roomAccess = roomInfo.value?.joinRule.map(), roomAccess = roomInfo.joinRule.map(),
isEncrypted = room.isEncrypted, isEncrypted = roomInfo.isEncrypted == true,
isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory.value, isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory.value,
historyVisibility = roomInfo.value?.historyVisibility.map(), historyVisibility = historyVisibility,
address = roomInfo.value?.firstDisplayableAlias(homeserverName)?.value, address = roomInfo.firstDisplayableAlias(homeserverName)?.value,
) )
} }
} }

View file

@ -9,6 +9,8 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroom
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -24,6 +26,7 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import io.element.android.libraries.matrix.api.roomAliasFromName import io.element.android.libraries.matrix.api.roomAliasFromName
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
@ -45,15 +48,16 @@ class EditRoomAddressPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): EditRoomAddressState { override fun present(): EditRoomAddressState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val roomInfo by room.roomInfoFlow.collectAsState()
val homeserverName = remember { client.userIdServerName() } val homeserverName = remember { client.userIdServerName() }
val roomAddressValidity = remember { val roomAddressValidity = remember {
mutableStateOf<RoomAddressValidity>(RoomAddressValidity.Unknown) mutableStateOf<RoomAddressValidity>(RoomAddressValidity.Unknown)
} }
val savedRoomAddress = remember { room.firstAliasMatching(homeserverName)?.addressName() } val savedRoomAddress by remember { derivedStateOf { roomInfo.firstAliasMatching(homeserverName)?.addressName() } }
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) } val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
var newRoomAddress by remember { var newRoomAddress by remember {
mutableStateOf( mutableStateOf(
savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(room.displayName) savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(roomInfo.name.orEmpty())
) )
} }
@ -97,8 +101,9 @@ class EditRoomAddressPresenter @AssistedInject constructor(
newRoomAddress: String, newRoomAddress: String,
) = launch { ) = launch {
suspend { suspend {
val savedCanonicalAlias = room.canonicalAlias val roomInfo = room.info()
val savedAliasFromHomeserver = room.firstAliasMatching(serverName) val savedCanonicalAlias = roomInfo.canonicalAlias
val savedAliasFromHomeserver = roomInfo.firstAliasMatching(serverName)
val newRoomAlias = client.roomAliasFromName(newRoomAddress) ?: throw IllegalArgumentException("Invalid room address") val newRoomAlias = client.roomAliasFromName(newRoomAddress) ?: throw IllegalArgumentException("Invalid room address")
// First publish the new alias in the room directory // First publish the new alias in the room directory
@ -112,7 +117,7 @@ class EditRoomAddressPresenter @AssistedInject constructor(
when { when {
// Allow to update the canonical alias only if the saved canonical alias matches the homeserver or if there is no canonical alias // Allow to update the canonical alias only if the saved canonical alias matches the homeserver or if there is no canonical alias
savedCanonicalAlias == null || savedCanonicalAlias.matchesServer(serverName) -> { savedCanonicalAlias == null || savedCanonicalAlias.matchesServer(serverName) -> {
val newAlternativeAliases = room.alternativeAliases.filter { it != savedAliasFromHomeserver } val newAlternativeAliases = roomInfo.alternativeAliases.filter { it != savedAliasFromHomeserver }
room.updateCanonicalAlias(newRoomAlias, newAlternativeAliases).getOrThrow() room.updateCanonicalAlias(newRoomAlias, newAlternativeAliases).getOrThrow()
} }
// Otherwise, only update the alternative aliases and keep the current canonical alias // Otherwise, only update the alternative aliases and keep the current canonical alias
@ -121,7 +126,7 @@ class EditRoomAddressPresenter @AssistedInject constructor(
// New alias is added first, so we make sure we pick it first // New alias is added first, so we make sure we pick it first
add(newRoomAlias) add(newRoomAlias)
// Add all other aliases, except the one we just removed from the room directory // Add all other aliases, except the one we just removed from the room directory
addAll(room.alternativeAliases.filter { it != savedAliasFromHomeserver }) addAll(roomInfo.alternativeAliases.filter { it != savedAliasFromHomeserver })
} }
room.updateCanonicalAlias(savedCanonicalAlias, newAlternativeAliases).getOrThrow() room.updateCanonicalAlias(savedCanonicalAlias, newAlternativeAliases).getOrThrow()
} }
@ -134,7 +139,7 @@ class EditRoomAddressPresenter @AssistedInject constructor(
/** /**
* Returns the first alias that matches the given server name, or null if none match. * Returns the first alias that matches the given server name, or null if none match.
*/ */
private fun MatrixRoom.firstAliasMatching(serverName: String): RoomAlias? { private fun MatrixRoomInfo.firstAliasMatching(serverName: String): RoomAlias? {
// Check if the canonical alias matches the homeserver // Check if the canonical alias matches the homeserver
if (canonicalAlias?.matchesServer(serverName) == true) { if (canonicalAlias?.matchesServer(serverName) == true) {
return canonicalAlias return canonicalAlias

View file

@ -7,34 +7,43 @@
package io.element.android.features.roomdetails.impl package io.element.android.features.roomdetails.impl
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.join.JoinRule
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.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
fun aMatrixRoom( fun aMatrixRoom(
sessionId: SessionId = A_SESSION_ID,
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
displayName: String = A_ROOM_NAME, displayName: String = A_ROOM_NAME,
rawName: String? = displayName, rawName: String? = displayName,
topic: String? = A_ROOM_TOPIC, topic: String? = A_ROOM_TOPIC,
avatarUrl: String? = AN_AVATAR_URL, avatarUrl: String? = AN_AVATAR_URL,
canonicalAlias: RoomAlias? = A_ROOM_ALIAS,
isEncrypted: Boolean = true, isEncrypted: Boolean = true,
isPublic: Boolean = true, isPublic: Boolean = true,
isDirect: Boolean = false, isDirect: Boolean = false,
joinRule: JoinRule? = null, joinRule: JoinRule? = null,
activeMemberCount: Long = 1,
joinedMemberCount: Long = 1,
invitedMemberCount: Long = 0,
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
emitRoomInfo: Boolean = false,
canInviteResult: (UserId) -> Result<Boolean> = { lambdaError() }, canInviteResult: (UserId) -> Result<Boolean> = { lambdaError() },
canBanResult: (UserId) -> Result<Boolean> = { lambdaError() }, canBanResult: (UserId) -> Result<Boolean> = { lambdaError() },
canKickResult: (UserId) -> Result<Boolean> = { lambdaError() },
canSendStateResult: (UserId, StateEventType) -> Result<Boolean> = { _, _ -> lambdaError() }, canSendStateResult: (UserId, StateEventType) -> Result<Boolean> = { _, _ -> lambdaError() },
userDisplayNameResult: (UserId) -> Result<String?> = { lambdaError() }, userDisplayNameResult: (UserId) -> Result<String?> = { lambdaError() },
userAvatarUrlResult: () -> Result<String?> = { lambdaError() }, userAvatarUrlResult: () -> Result<String?> = { lambdaError() },
@ -44,17 +53,20 @@ fun aMatrixRoom(
removeAvatarResult: () -> Result<Unit> = { lambdaError() }, removeAvatarResult: () -> Result<Unit> = { lambdaError() },
canUserJoinCallResult: (UserId) -> Result<Boolean> = { lambdaError() }, canUserJoinCallResult: (UserId) -> Result<Boolean> = { lambdaError() },
getUpdatedMemberResult: (UserId) -> Result<RoomMember> = { lambdaError() }, getUpdatedMemberResult: (UserId) -> Result<RoomMember> = { lambdaError() },
userRoleResult: () -> Result<RoomMember.Role> = { lambdaError() },
kickUserResult: (UserId, String?) -> Result<Unit> = { _, _ -> lambdaError() },
banUserResult: (UserId, String?) -> Result<Unit> = { _, _ -> lambdaError() },
unBanUserResult: (UserId, String?) -> Result<Unit> = { _, _ -> lambdaError() },
updateCanonicalAliasResult: (RoomAlias?, List<RoomAlias>) -> Result<Unit> = { _, _ -> lambdaError() },
publishRoomAliasInRoomDirectoryResult: (RoomAlias) -> Result<Boolean> = { lambdaError() },
removeRoomAliasFromRoomDirectoryResult: (RoomAlias) -> Result<Boolean> = { lambdaError() },
) = FakeMatrixRoom( ) = FakeMatrixRoom(
sessionId = sessionId,
roomId = roomId, roomId = roomId,
displayName = displayName,
topic = topic,
avatarUrl = avatarUrl,
isEncrypted = isEncrypted,
isPublic = isPublic,
isDirect = isDirect,
notificationSettingsService = notificationSettingsService, notificationSettingsService = notificationSettingsService,
canInviteResult = canInviteResult, canInviteResult = canInviteResult,
canBanResult = canBanResult, canBanResult = canBanResult,
canKickResult = canKickResult,
canSendStateResult = canSendStateResult, canSendStateResult = canSendStateResult,
userDisplayNameResult = userDisplayNameResult, userDisplayNameResult = userDisplayNameResult,
userAvatarUrlResult = userAvatarUrlResult, userAvatarUrlResult = userAvatarUrlResult,
@ -64,17 +76,25 @@ fun aMatrixRoom(
removeAvatarResult = removeAvatarResult, removeAvatarResult = removeAvatarResult,
canUserJoinCallResult = canUserJoinCallResult, canUserJoinCallResult = canUserJoinCallResult,
getUpdatedMemberResult = getUpdatedMemberResult, getUpdatedMemberResult = getUpdatedMemberResult,
).apply { userRoleResult = userRoleResult,
if (emitRoomInfo) { kickUserResult = kickUserResult,
givenRoomInfo( banUserResult = banUserResult,
aRoomInfo( unBanUserResult = unBanUserResult,
name = displayName, updateCanonicalAliasResult = updateCanonicalAliasResult,
rawName = rawName, publishRoomAliasInRoomDirectoryResult = publishRoomAliasInRoomDirectoryResult,
topic = topic, removeRoomAliasFromRoomDirectoryResult = removeRoomAliasFromRoomDirectoryResult,
avatarUrl = avatarUrl, initialRoomInfo = aRoomInfo(
isDirect = isDirect, name = displayName,
joinRule = joinRule, rawName = rawName,
) topic = topic,
) avatarUrl = avatarUrl,
} canonicalAlias = canonicalAlias,
} isDirect = isDirect,
isPublic = isPublic,
isEncrypted = isEncrypted,
joinRule = joinRule,
joinedMembersCount = joinedMemberCount,
activeMembersCount = activeMemberCount,
invitedMembersCount = invitedMemberCount,
)
)

View file

@ -110,7 +110,7 @@ class RoomDetailsPresenterTest {
} }
@Test @Test
fun `present - initial state is created from room if roomInfo is null`() = runTest { fun `present - initial state is created from initial room info`() = runTest {
val room = aMatrixRoom( val room = aMatrixRoom(
canInviteResult = { Result.success(true) }, canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) },
@ -118,22 +118,22 @@ class RoomDetailsPresenterTest {
) )
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.roomId).isEqualTo(room.roomId) assertThat(initialState.roomId).isEqualTo(room.roomId)
assertThat(initialState.roomName).isEqualTo(room.displayName) assertThat(initialState.roomName).isEqualTo(room.info().name)
assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl) assertThat(initialState.roomAvatarUrl).isEqualTo(room.info().avatarUrl)
assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!)) assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.info().topic!!))
assertThat(initialState.memberCount).isEqualTo(room.joinedMemberCount) assertThat(initialState.memberCount).isEqualTo(room.info().joinedMembersCount)
assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted)
assertThat(initialState.canShowPinnedMessages).isTrue() assertThat(initialState.canShowPinnedMessages).isTrue()
assertThat(initialState.pinnedMessagesCount).isNull() assertThat(initialState.pinnedMessagesCount).isEqualTo(0)
assertThat(initialState.canShowSecurityAndPrivacy).isFalse() assertThat(initialState.canShowSecurityAndPrivacy).isFalse()
cancelAndIgnoreRemainingEvents()
} }
} }
@Test @Test
fun `present - initial state is updated with roomInfo if it exists`() = runTest { fun `present - initial state is updated with a new roomInfo`() = runTest {
val roomInfo = aRoomInfo( val roomInfo = aRoomInfo(
name = A_ROOM_NAME, name = A_ROOM_NAME,
topic = A_ROOM_TOPIC, topic = A_ROOM_TOPIC,
@ -170,7 +170,7 @@ class RoomDetailsPresenterTest {
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.roomName).isEqualTo(room.displayName) assertThat(initialState.roomName).isEqualTo(room.info().name)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
@ -181,8 +181,6 @@ class RoomDetailsPresenterTest {
val myRoomMember = aRoomMember(A_SESSION_ID) val myRoomMember = aRoomMember(A_SESSION_ID)
val otherRoomMember = aRoomMember(A_USER_ID_2) val otherRoomMember = aRoomMember(A_USER_ID_2)
val room = aMatrixRoom( val room = aMatrixRoom(
isEncrypted = true,
isDirect = true,
canInviteResult = { Result.success(true) }, canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) },
@ -196,6 +194,13 @@ class RoomDetailsPresenterTest {
).apply { ).apply {
val roomMembers = persistentListOf(myRoomMember, otherRoomMember) val roomMembers = persistentListOf(myRoomMember, otherRoomMember)
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
givenRoomInfo(
aRoomInfo(
isEncrypted = true,
isDirect = true,
)
)
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) {
@ -287,8 +292,6 @@ class RoomDetailsPresenterTest {
val myRoomMember = aRoomMember(A_SESSION_ID) val myRoomMember = aRoomMember(A_SESSION_ID)
val otherRoomMember = aRoomMember(A_USER_ID_2) val otherRoomMember = aRoomMember(A_USER_ID_2)
val room = aMatrixRoom( val room = aMatrixRoom(
isEncrypted = true,
isDirect = true,
canSendStateResult = { _, stateEventType -> canSendStateResult = { _, stateEventType ->
when (stateEventType) { when (stateEventType) {
StateEventType.ROOM_TOPIC, StateEventType.ROOM_TOPIC,
@ -309,6 +312,13 @@ class RoomDetailsPresenterTest {
).apply { ).apply {
val roomMembers = persistentListOf(myRoomMember, otherRoomMember) val roomMembers = persistentListOf(myRoomMember, otherRoomMember)
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
givenRoomInfo(
aRoomInfo(
isEncrypted = true,
isDirect = true,
)
)
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) {
@ -318,7 +328,7 @@ class RoomDetailsPresenterTest {
val settledState = awaitItem() val settledState = awaitItem()
assertThat(settledState.canEdit).isFalse() assertThat(settledState.canEdit).isFalse()
// If there is a topic, it's visible // If there is a topic, it's visible
assertThat(settledState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!)) assertThat(settledState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.info().topic!!))
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
@ -329,7 +339,6 @@ class RoomDetailsPresenterTest {
val myRoomMember = aRoomMember(A_SESSION_ID) val myRoomMember = aRoomMember(A_SESSION_ID)
val otherRoomMember = aRoomMember(A_USER_ID_2) val otherRoomMember = aRoomMember(A_USER_ID_2)
val room = aMatrixRoom( val room = aMatrixRoom(
isEncrypted = true,
isDirect = true, isDirect = true,
topic = null, topic = null,
canSendStateResult = { _, stateEventType -> canSendStateResult = { _, stateEventType ->
@ -352,6 +361,14 @@ class RoomDetailsPresenterTest {
).apply { ).apply {
val roomMembers = persistentListOf(myRoomMember, otherRoomMember) val roomMembers = persistentListOf(myRoomMember, otherRoomMember)
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
givenRoomInfo(
aRoomInfo(
isDirect = true,
activeMembersCount = 2,
topic = null,
)
)
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
@ -630,7 +647,6 @@ class RoomDetailsPresenterTest {
@Test @Test
fun `present - show knock requests`() = runTest { fun `present - show knock requests`() = runTest {
val room = aMatrixRoom( val room = aMatrixRoom(
emitRoomInfo = true,
canInviteResult = { Result.success(true) }, canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) },

View file

@ -93,7 +93,6 @@ class RoomDetailsEditPresenterTest {
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
displayName = A_ROOM_NAME, displayName = A_ROOM_NAME,
rawName = A_ROOM_RAW_NAME, rawName = A_ROOM_RAW_NAME,
emitRoomInfo = true,
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
val deleteCallback = lambdaRecorder<Uri?, Unit> {} val deleteCallback = lambdaRecorder<Uri?, Unit> {}
@ -106,7 +105,7 @@ class RoomDetailsEditPresenterTest {
assertThat(initialState.roomId).isEqualTo(room.roomId) assertThat(initialState.roomId).isEqualTo(room.roomId)
assertThat(initialState.roomRawName).isEqualTo(A_ROOM_RAW_NAME) assertThat(initialState.roomRawName).isEqualTo(A_ROOM_RAW_NAME)
assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri) assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri)
assertThat(initialState.roomTopic).isEqualTo(room.topic.orEmpty()) assertThat(initialState.roomTopic).isEqualTo(room.info().topic.orEmpty())
assertThat(initialState.avatarActions).containsExactly( assertThat(initialState.avatarActions).containsExactly(
AvatarAction.ChoosePhoto, AvatarAction.ChoosePhoto,
AvatarAction.TakePhoto, AvatarAction.TakePhoto,
@ -220,7 +219,6 @@ class RoomDetailsEditPresenterTest {
topic = "My topic", topic = "My topic",
displayName = "Name", displayName = "Name",
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
val deleteCallback = lambdaRecorder<Uri?, Unit> {} val deleteCallback = lambdaRecorder<Uri?, Unit> {}
@ -266,7 +264,6 @@ class RoomDetailsEditPresenterTest {
topic = "My topic", topic = "My topic",
displayName = "Name", displayName = "Name",
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
fakePickerProvider.givenResult(anotherAvatarUri) fakePickerProvider.givenResult(anotherAvatarUri)
@ -291,7 +288,6 @@ class RoomDetailsEditPresenterTest {
topic = "My topic", topic = "My topic",
displayName = "Name", displayName = "Name",
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
fakePickerProvider.givenResult(anotherAvatarUri) fakePickerProvider.givenResult(anotherAvatarUri)
@ -319,8 +315,7 @@ class RoomDetailsEditPresenterTest {
stateWithNewAvatar.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto)) stateWithNewAvatar.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto))
val stateWithNewAvatar2 = awaitItem() val stateWithNewAvatar2 = awaitItem()
assertThat(stateWithNewAvatar2.roomAvatarUrl).isEqualTo(roomAvatarUri) assertThat(stateWithNewAvatar2.roomAvatarUrl).isEqualTo(roomAvatarUri)
deleteCallback.assertions().isCalledExactly(4).withSequence( deleteCallback.assertions().isCalledExactly(3).withSequence(
listOf(value(null)),
listOf(value(null)), listOf(value(null)),
listOf(value(roomAvatarUri)), listOf(value(roomAvatarUri)),
listOf(value(anotherAvatarUri)), listOf(value(anotherAvatarUri)),
@ -334,7 +329,6 @@ class RoomDetailsEditPresenterTest {
topic = "My topic", topic = "My topic",
displayName = "Name", displayName = "Name",
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
fakePickerProvider.givenResult(roomAvatarUri) fakePickerProvider.givenResult(roomAvatarUri)
@ -385,7 +379,6 @@ class RoomDetailsEditPresenterTest {
topic = null, topic = null,
displayName = "fallback", displayName = "fallback",
avatarUrl = null, avatarUrl = null,
emitRoomInfo = true,
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
fakePickerProvider.givenResult(roomAvatarUri) fakePickerProvider.givenResult(roomAvatarUri)
@ -439,7 +432,6 @@ class RoomDetailsEditPresenterTest {
topic = "My topic", topic = "My topic",
displayName = "Name", displayName = "Name",
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
setNameResult = setNameResult, setNameResult = setNameResult,
setTopicResult = setTopicResult, setTopicResult = setTopicResult,
removeAvatarResult = removeAvatarResult, removeAvatarResult = removeAvatarResult,
@ -553,7 +545,7 @@ class RoomDetailsEditPresenterTest {
updateAvatarResult.assertions().isCalledOnce().with(value(MimeTypes.Jpeg), value(fakeFileContents)) updateAvatarResult.assertions().isCalledOnce().with(value(MimeTypes.Jpeg), value(fakeFileContents))
deleteCallback.assertions().isCalledExactly(2).withSequence( deleteCallback.assertions().isCalledExactly(2).withSequence(
listOf(value(null)), listOf(value(null)),
listOf(value(null)), listOf(value(roomAvatarUri)),
) )
} }
} }
@ -588,11 +580,10 @@ class RoomDetailsEditPresenterTest {
topic = "My topic", topic = "My topic",
displayName = "Name", displayName = "Name",
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
setNameResult = { Result.failure(Throwable("!")) }, setNameResult = { Result.failure(Throwable("!")) },
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomName("New name")) saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomName("New name"), deleteCallbackNumberOfInvocation = 1)
} }
@Test @Test
@ -601,11 +592,10 @@ class RoomDetailsEditPresenterTest {
topic = "My topic", topic = "My topic",
displayName = "Name", displayName = "Name",
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
setTopicResult = { Result.failure(Throwable("!")) }, setTopicResult = { Result.failure(Throwable("!")) },
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomTopic("New topic")) saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomTopic("New topic"), deleteCallbackNumberOfInvocation = 1)
} }
@Test @Test
@ -614,11 +604,10 @@ class RoomDetailsEditPresenterTest {
topic = "My topic", topic = "My topic",
displayName = "Name", displayName = "Name",
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
removeAvatarResult = { Result.failure(Throwable("!")) }, removeAvatarResult = { Result.failure(Throwable("!")) },
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove), deleteCallbackNumberOfInvocation = 3) saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove), deleteCallbackNumberOfInvocation = 2)
} }
@Test @Test
@ -628,11 +617,10 @@ class RoomDetailsEditPresenterTest {
topic = "My topic", topic = "My topic",
displayName = "Name", displayName = "Name",
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
updateAvatarResult = { _, _ -> Result.failure(Throwable("!")) }, updateAvatarResult = { _, _ -> Result.failure(Throwable("!")) },
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto), deleteCallbackNumberOfInvocation = 3) saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto), deleteCallbackNumberOfInvocation = 2)
} }
@Test @Test
@ -704,6 +692,6 @@ class RoomDetailsEditPresenterTest {
} }
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T { private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(2) skipItems(1)
return awaitItem() return awaitItem()
} }

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -167,10 +168,10 @@ class RoomMemberDetailsPresenterTest {
@Test @Test
fun `present - when user's identity is verified, the value in the state is VERIFIED`() = runTest { fun `present - when user's identity is verified, the value in the state is VERIFIED`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
isEncrypted = true,
getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) },
userDisplayNameResult = { Result.success("A custom name") }, userDisplayNameResult = { Result.success("A custom name") },
userAvatarUrlResult = { Result.success("A custom avatar") }, userAvatarUrlResult = { Result.success("A custom avatar") },
initialRoomInfo = aRoomInfo(isEncrypted = true),
) )
val encryptionService = FakeEncryptionService( val encryptionService = FakeEncryptionService(
getUserIdentityResult = { Result.success(IdentityState.Verified) }, getUserIdentityResult = { Result.success(IdentityState.Verified) },
@ -186,10 +187,10 @@ class RoomMemberDetailsPresenterTest {
@Test @Test
fun `present - when user's identity is unknown, the value in the state is UNKNOWN`() = runTest { fun `present - when user's identity is unknown, the value in the state is UNKNOWN`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
isEncrypted = true,
getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) },
userDisplayNameResult = { Result.success("A custom name") }, userDisplayNameResult = { Result.success("A custom name") },
userAvatarUrlResult = { Result.success("A custom avatar") }, userAvatarUrlResult = { Result.success("A custom avatar") },
initialRoomInfo = aRoomInfo(isEncrypted = true),
) )
val encryptionService = FakeEncryptionService( val encryptionService = FakeEncryptionService(
getUserIdentityResult = { Result.success(null) }, getUserIdentityResult = { Result.success(null) },
@ -205,10 +206,10 @@ class RoomMemberDetailsPresenterTest {
@Test @Test
fun `present - when user's identity is pinned, the value in the state is UNVERIFIED`() = runTest { fun `present - when user's identity is pinned, the value in the state is UNVERIFIED`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
isEncrypted = true,
getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) },
userDisplayNameResult = { Result.success("A custom name") }, userDisplayNameResult = { Result.success("A custom name") },
userAvatarUrlResult = { Result.success("A custom avatar") }, userAvatarUrlResult = { Result.success("A custom avatar") },
initialRoomInfo = aRoomInfo(isEncrypted = true),
) )
val encryptionService = FakeEncryptionService( val encryptionService = FakeEncryptionService(
getUserIdentityResult = { Result.success(IdentityState.Pinned) }, getUserIdentityResult = { Result.success(IdentityState.Pinned) },
@ -224,10 +225,10 @@ class RoomMemberDetailsPresenterTest {
@Test @Test
fun `present - when user's identity is pin violation, the value in the state is UNVERIFIED`() = runTest { fun `present - when user's identity is pin violation, the value in the state is UNVERIFIED`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
isEncrypted = true,
getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) },
userDisplayNameResult = { Result.success("A custom name") }, userDisplayNameResult = { Result.success("A custom name") },
userAvatarUrlResult = { Result.success("A custom avatar") }, userAvatarUrlResult = { Result.success("A custom avatar") },
initialRoomInfo = aRoomInfo(isEncrypted = true),
) )
val encryptionService = FakeEncryptionService( val encryptionService = FakeEncryptionService(
getUserIdentityResult = { Result.success(IdentityState.PinViolation) }, getUserIdentityResult = { Result.success(IdentityState.PinViolation) },
@ -243,10 +244,10 @@ class RoomMemberDetailsPresenterTest {
@Test @Test
fun `present - when user's identity has a verification violation, the value in the state is VERIFICATION_VIOLATION`() = runTest { fun `present - when user's identity has a verification violation, the value in the state is VERIFICATION_VIOLATION`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
isEncrypted = true,
getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) },
userDisplayNameResult = { Result.success("A custom name") }, userDisplayNameResult = { Result.success("A custom name") },
userAvatarUrlResult = { Result.success("A custom avatar") }, userAvatarUrlResult = { Result.success("A custom avatar") },
initialRoomInfo = aRoomInfo(isEncrypted = true),
) )
val encryptionService = FakeEncryptionService( val encryptionService = FakeEncryptionService(
getUserIdentityResult = { Result.success(IdentityState.VerificationViolation) }, getUserIdentityResult = { Result.success(IdentityState.VerificationViolation) },
@ -262,10 +263,10 @@ class RoomMemberDetailsPresenterTest {
@Test @Test
fun `present - user identity updates in real time if the room is encrypted`() = runTest { fun `present - user identity updates in real time if the room is encrypted`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
isEncrypted = true,
getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) },
userDisplayNameResult = { Result.success("A custom name") }, userDisplayNameResult = { Result.success("A custom name") },
userAvatarUrlResult = { Result.success("A custom avatar") }, userAvatarUrlResult = { Result.success("A custom avatar") },
initialRoomInfo = aRoomInfo(isEncrypted = true),
) )
val encryptionService = FakeEncryptionService( val encryptionService = FakeEncryptionService(
getUserIdentityResult = { Result.success(null) }, getUserIdentityResult = { Result.success(null) },
@ -291,10 +292,10 @@ class RoomMemberDetailsPresenterTest {
@Test @Test
fun `present - user identity can't update in real time if the room is not encrypted`() = runTest { fun `present - user identity can't update in real time if the room is not encrypted`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
isEncrypted = false,
getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) },
userDisplayNameResult = { Result.success("A custom name") }, userDisplayNameResult = { Result.success("A custom name") },
userAvatarUrlResult = { Result.success("A custom avatar") }, userAvatarUrlResult = { Result.success("A custom avatar") },
initialRoomInfo = aRoomInfo(isEncrypted = false),
) )
val encryptionService = FakeEncryptionService( val encryptionService = FakeEncryptionService(
getUserIdentityResult = { Result.success(null) }, getUserIdentityResult = { Result.success(null) },
@ -315,10 +316,10 @@ class RoomMemberDetailsPresenterTest {
@Test @Test
fun `present - handles WithdrawVerification action`() = runTest { fun `present - handles WithdrawVerification action`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
isEncrypted = true,
getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) },
userDisplayNameResult = { Result.success("A custom name") }, userDisplayNameResult = { Result.success("A custom name") },
userAvatarUrlResult = { Result.success("A custom avatar") }, userAvatarUrlResult = { Result.success("A custom avatar") },
initialRoomInfo = aRoomInfo(isEncrypted = true),
) )
val withdrawVerificationResult = lambdaRecorder<UserId, Result<Unit>> { Result.success(Unit) } val withdrawVerificationResult = lambdaRecorder<UserId, Result<Unit>> { Result.success(Unit) }
val encryptionService = FakeEncryptionService( val encryptionService = FakeEncryptionService(

View file

@ -12,6 +12,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.RoomModeration import im.vector.app.features.analytics.plan.RoomModeration
import io.element.android.features.roomdetails.impl.aMatrixRoom
import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.features.roomdetails.impl.members.aVictor import io.element.android.features.roomdetails.impl.members.aVictor
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -34,8 +35,7 @@ import org.junit.Test
class RoomMembersModerationPresenterTest { class RoomMembersModerationPresenterTest {
@Test @Test
fun `canDisplayModerationActions - when room is DM is false`() = runTest { fun `canDisplayModerationActions - when room is DM is false`() = runTest {
val room = FakeMatrixRoom( val room = aMatrixRoom(
isDirect = true,
isPublic = true, isPublic = true,
activeMemberCount = 2, activeMemberCount = 2,
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
@ -52,8 +52,7 @@ class RoomMembersModerationPresenterTest {
@Test @Test
fun `canDisplayModerationActions - when user can kick other users, FF is enabled and room is not a DM returns true`() = runTest { fun `canDisplayModerationActions - when user can kick other users, FF is enabled and room is not a DM returns true`() = runTest {
val room = FakeMatrixRoom( val room = aMatrixRoom(
isDirect = false,
activeMemberCount = 10, activeMemberCount = 10,
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
@ -68,8 +67,7 @@ class RoomMembersModerationPresenterTest {
@Test @Test
fun `canDisplayModerationActions - when user can ban other users, FF is enabled and room is not a DM returns true`() = runTest { fun `canDisplayModerationActions - when user can ban other users, FF is enabled and room is not a DM returns true`() = runTest {
val room = FakeMatrixRoom( val room = aMatrixRoom(
isDirect = false,
activeMemberCount = 10, activeMemberCount = 10,
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
@ -84,7 +82,7 @@ class RoomMembersModerationPresenterTest {
@Test @Test
fun `present - SelectRoomMember when the current user has permissions displays member actions`() = runTest { fun `present - SelectRoomMember when the current user has permissions displays member actions`() = runTest {
val room = FakeMatrixRoom( val room = aMatrixRoom(
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
@ -110,7 +108,7 @@ class RoomMembersModerationPresenterTest {
@Test @Test
fun `present - SelectRoomMember displays only view profile if selected member has same power level as the current user`() = runTest { fun `present - SelectRoomMember displays only view profile if selected member has same power level as the current user`() = runTest {
val room = FakeMatrixRoom( val room = aMatrixRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
@ -136,7 +134,7 @@ class RoomMembersModerationPresenterTest {
@Test @Test
fun `present - SelectRoomMember displays an unban confirmation dialog when the member is banned`() = runTest { fun `present - SelectRoomMember displays an unban confirmation dialog when the member is banned`() = runTest {
val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN) val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN)
val room = FakeMatrixRoom( val room = aMatrixRoom(
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
@ -157,7 +155,7 @@ class RoomMembersModerationPresenterTest {
@Test @Test
fun `present - Kick removes the user`() = runTest { fun `present - Kick removes the user`() = runTest {
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val room = FakeMatrixRoom( val room = aMatrixRoom(
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
@ -186,7 +184,7 @@ class RoomMembersModerationPresenterTest {
@Test @Test
fun `present - BanUser requires confirmation and then bans the user`() = runTest { fun `present - BanUser requires confirmation and then bans the user`() = runTest {
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val room = FakeMatrixRoom( val room = aMatrixRoom(
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
@ -222,7 +220,7 @@ class RoomMembersModerationPresenterTest {
fun `present - UnbanUser requires confirmation and then unbans the user`() = runTest { fun `present - UnbanUser requires confirmation and then unbans the user`() = runTest {
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN) val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN)
val room = FakeMatrixRoom( val room = aMatrixRoom(
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
@ -254,7 +252,7 @@ class RoomMembersModerationPresenterTest {
@Test @Test
fun `present - Reset removes the selected user and actions`() = runTest { fun `present - Reset removes the selected user and actions`() = runTest {
val room = FakeMatrixRoom( val room = aMatrixRoom(
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.USER) }, userRoleResult = { Result.success(RoomMember.Role.USER) },
@ -276,7 +274,7 @@ class RoomMembersModerationPresenterTest {
@Test @Test
fun `present - Reset resets any async actions`() = runTest { fun `present - Reset resets any async actions`() = runTest {
val room = FakeMatrixRoom( val room = aMatrixRoom(
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
kickUserResult = { _, _ -> Result.failure(Throwable("Eek")) }, kickUserResult = { _, _ -> Result.failure(Throwable("Eek")) },
@ -324,7 +322,7 @@ class RoomMembersModerationPresenterTest {
} }
private fun TestScope.createRoomMembersModerationPresenter( private fun TestScope.createRoomMembersModerationPresenter(
matrixRoom: FakeMatrixRoom = FakeMatrixRoom(), matrixRoom: FakeMatrixRoom = aMatrixRoom(),
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
): RoomMembersModerationPresenter { ): RoomMembersModerationPresenter {

View file

@ -56,17 +56,15 @@ class SecurityAndPrivacyPresenterTest {
fun `present - room info change updates saved and edited settings`() = runTest { fun `present - room info change updates saved and edited settings`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
canSendStateResult = { _, _ -> Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) },
initialRoomInfo = aRoomInfo(
joinRule = JoinRule.Public,
historyVisibility = RoomHistoryVisibility.WorldReadable,
canonicalAlias = A_ROOM_ALIAS,
)
) )
val presenter = createSecurityAndPrivacyPresenter(room = room) val presenter = createSecurityAndPrivacyPresenter(room = room)
presenter.test { presenter.test {
skipItems(2) skipItems(1)
room.givenRoomInfo(
aRoomInfo(
joinRule = JoinRule.Public,
historyVisibility = RoomHistoryVisibility.WorldReadable,
canonicalAlias = A_ROOM_ALIAS,
)
)
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings).isEqualTo(savedSettings) assertThat(editedSettings).isEqualTo(savedSettings)
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone)
@ -151,6 +149,7 @@ class SecurityAndPrivacyPresenterTest {
assertThat(canBeSaved).isTrue() assertThat(canBeSaved).isTrue()
eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState)
} }
skipItems(1)
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.isEncrypted).isFalse() assertThat(editedSettings.isEncrypted).isFalse()
assertThat(canBeSaved).isFalse() assertThat(canBeSaved).isFalse()
@ -162,7 +161,8 @@ class SecurityAndPrivacyPresenterTest {
fun `present - room visibility loading and change`() = runTest { fun `present - room visibility loading and change`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
canSendStateResult = { _, _ -> Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) },
roomVisibilityResult = { Result.success(RoomVisibility.Private) } roomVisibilityResult = { Result.success(RoomVisibility.Private) },
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared)
) )
val presenter = createSecurityAndPrivacyPresenter(room = room) val presenter = createSecurityAndPrivacyPresenter(room = room)
presenter.test { presenter.test {
@ -212,7 +212,8 @@ class SecurityAndPrivacyPresenterTest {
updateJoinRuleResult = updateJoinRuleLambda, updateJoinRuleResult = updateJoinRuleLambda,
updateRoomVisibilityResult = updateRoomVisibilityLambda, updateRoomVisibilityResult = updateRoomVisibilityLambda,
updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda, updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda,
roomVisibilityResult = { Result.success(RoomVisibility.Private) } roomVisibilityResult = { Result.success(RoomVisibility.Private) },
initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite, historyVisibility = RoomHistoryVisibility.Shared)
) )
val presenter = createSecurityAndPrivacyPresenter(room = room) val presenter = createSecurityAndPrivacyPresenter(room = room)
presenter.test { presenter.test {
@ -245,6 +246,7 @@ class SecurityAndPrivacyPresenterTest {
aRoomInfo( aRoomInfo(
joinRule = JoinRule.Public, joinRule = JoinRule.Public,
historyVisibility = RoomHistoryVisibility.WorldReadable, historyVisibility = RoomHistoryVisibility.WorldReadable,
isEncrypted = true,
) )
) )
// Saved settings are updated 3 times to match the edited settings // Saved settings are updated 3 times to match the edited settings
@ -275,7 +277,8 @@ class SecurityAndPrivacyPresenterTest {
updateJoinRuleResult = updateJoinRuleLambda, updateJoinRuleResult = updateJoinRuleLambda,
updateRoomVisibilityResult = updateRoomVisibilityLambda, updateRoomVisibilityResult = updateRoomVisibilityLambda,
updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda, updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda,
roomVisibilityResult = { Result.success(RoomVisibility.Private) } roomVisibilityResult = { Result.success(RoomVisibility.Private) },
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private)
) )
val presenter = createSecurityAndPrivacyPresenter(room = room) val presenter = createSecurityAndPrivacyPresenter(room = room)
presenter.test { presenter.test {
@ -311,7 +314,7 @@ class SecurityAndPrivacyPresenterTest {
) )
) )
// Saved settings are updated 2 times to match the edited settings // Saved settings are updated 2 times to match the edited settings
skipItems(2) skipItems(3)
with(awaitItem()) { with(awaitItem()) {
assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat(savedSettings.isVisibleInRoomDirectory).isNotEqualTo(editedSettings.isVisibleInRoomDirectory) assertThat(savedSettings.isVisibleInRoomDirectory).isNotEqualTo(editedSettings.isVisibleInRoomDirectory)
@ -328,7 +331,8 @@ class SecurityAndPrivacyPresenterTest {
serverName: String = "matrix.org", serverName: String = "matrix.org",
room: MatrixRoom = FakeMatrixRoom( room: MatrixRoom = FakeMatrixRoom(
canSendStateResult = { _, _ -> Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) },
roomVisibilityResult = { Result.success(RoomVisibility.Private) } roomVisibilityResult = { Result.success(RoomVisibility.Private) },
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private)
), ),
navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(), navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(),
): SecurityAndPrivacyPresenter { ): SecurityAndPrivacyPresenter {

View file

@ -8,6 +8,7 @@
package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.roomdetails.impl.aMatrixRoom
import io.element.android.features.roomdetails.impl.securityandprivacy.FakeSecurityAndPrivacyNavigator import io.element.android.features.roomdetails.impl.securityandprivacy.FakeSecurityAndPrivacyNavigator
import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -31,7 +32,9 @@ import java.util.Optional
class EditRoomAddressPresenterTest { class EditRoomAddressPresenterTest {
@Test @Test
fun `present - initial state no address`() = runTest { fun `present - initial state no address`() = runTest {
val presenter = createEditRoomAddressPresenter() val presenter = createEditRoomAddressPresenter(
room = aMatrixRoom(displayName = "")
)
presenter.test { presenter.test {
with(awaitItem()) { with(awaitItem()) {
assertThat(homeserverName).isEqualTo("matrix.org") assertThat(homeserverName).isEqualTo("matrix.org")
@ -45,7 +48,7 @@ class EditRoomAddressPresenterTest {
@Test @Test
fun `present - initial state address matching own homeserver`() = runTest { fun `present - initial state address matching own homeserver`() = runTest {
val room = FakeMatrixRoom( val room = aMatrixRoom(
canonicalAlias = RoomAlias("#canonical:matrix.org"), canonicalAlias = RoomAlias("#canonical:matrix.org"),
) )
val presenter = createEditRoomAddressPresenter(room = room) val presenter = createEditRoomAddressPresenter(room = room)
@ -62,7 +65,8 @@ class EditRoomAddressPresenterTest {
@Test @Test
fun `present - initial state address not matching own homeserver`() = runTest { fun `present - initial state address not matching own homeserver`() = runTest {
val room = FakeMatrixRoom( val room = aMatrixRoom(
displayName = "",
canonicalAlias = RoomAlias("#canonical:notmatrix.org"), canonicalAlias = RoomAlias("#canonical:notmatrix.org"),
) )
val presenter = createEditRoomAddressPresenter(room = room) val presenter = createEditRoomAddressPresenter(room = room)
@ -190,7 +194,7 @@ class EditRoomAddressPresenterTest {
val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda) val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda)
val canonicalAlias = RoomAlias("#canonical:matrix.org") val canonicalAlias = RoomAlias("#canonical:matrix.org")
val room = FakeMatrixRoom( val room = aMatrixRoom(
canonicalAlias = canonicalAlias, canonicalAlias = canonicalAlias,
updateCanonicalAliasResult = updateCanonicalAliasResult, updateCanonicalAliasResult = updateCanonicalAliasResult,
publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult, publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult,
@ -240,7 +244,7 @@ class EditRoomAddressPresenterTest {
val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda) val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda)
val canonicalAlias = RoomAlias("#canonical:notmatrix.org") val canonicalAlias = RoomAlias("#canonical:notmatrix.org")
val room = FakeMatrixRoom( val room = aMatrixRoom(
canonicalAlias = canonicalAlias, canonicalAlias = canonicalAlias,
updateCanonicalAliasResult = updateCanonicalAliasResult, updateCanonicalAliasResult = updateCanonicalAliasResult,
publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult, publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult,
@ -314,6 +318,7 @@ class EditRoomAddressPresenterTest {
with(awaitItem()) { with(awaitItem()) {
eventSink(EditRoomAddressEvents.Save) eventSink(EditRoomAddressEvents.Save)
} }
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Loading::class.java)
with(awaitItem()) { with(awaitItem()) {
assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java)
eventSink(EditRoomAddressEvents.DismissError) eventSink(EditRoomAddressEvents.DismissError)

View file

@ -27,10 +27,12 @@ import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.time.Duration.Companion.seconds
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class IncomingVerificationPresenterTest { class IncomingVerificationPresenterTest {
@ -61,6 +63,9 @@ class IncomingVerificationPresenterTest {
isWaiting = false, isWaiting = false,
) )
) )
advanceTimeBy(1.seconds)
resetLambda.assertions().isCalledOnce().with(value(false)) resetLambda.assertions().isCalledOnce().with(value(false))
acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest))
acceptVerificationRequestLambda.assertions().isNeverCalled() acceptVerificationRequestLambda.assertions().isNeverCalled()
@ -69,7 +74,9 @@ class IncomingVerificationPresenterTest {
skipItems(1) skipItems(1)
val initialWaitingState = awaitItem() val initialWaitingState = awaitItem()
assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue() assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue()
advanceUntilIdle()
advanceTimeBy(1.seconds)
acceptVerificationRequestLambda.assertions().isCalledOnce() acceptVerificationRequestLambda.assertions().isCalledOnce()
// Remote sent the data // Remote sent the data
fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
@ -218,6 +225,9 @@ class IncomingVerificationPresenterTest {
isWaiting = false, isWaiting = false,
) )
) )
advanceTimeBy(1.seconds)
resetLambda.assertions().isCalledOnce().with(value(false)) resetLambda.assertions().isCalledOnce().with(value(false))
acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest))
acceptVerificationRequestLambda.assertions().isNeverCalled() acceptVerificationRequestLambda.assertions().isNeverCalled()
@ -226,7 +236,9 @@ class IncomingVerificationPresenterTest {
skipItems(1) skipItems(1)
val initialWaitingState = awaitItem() val initialWaitingState = awaitItem()
assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue() assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue()
advanceUntilIdle()
advanceTimeBy(1.seconds)
acceptVerificationRequestLambda.assertions().isCalledOnce() acceptVerificationRequestLambda.assertions().isCalledOnce()
// Remote sent the data // Remote sent the data
fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)

View file

@ -174,7 +174,7 @@ jsoup = "org.jsoup:jsoup:1.19.1"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0" molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.3.6" matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.3.13"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }

View file

@ -0,0 +1,25 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.core.coroutine
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
fun <T> suspendLazy(coroutineContext: CoroutineContext = EmptyCoroutineContext, block: suspend () -> T): Lazy<Deferred<T>> {
return lazy(LazyThreadSafetyMode.NONE) {
val deferred = CompletableDeferred<T>()
CoroutineScope(coroutineContext).launch {
deferred.complete(block())
}
deferred
}
}

View file

@ -18,8 +18,8 @@ fun MatrixRoom.toAnalyticsViewRoom(
val activeSpace = selectedSpace?.toActiveSpace() ?: ViewRoom.ActiveSpace.Home val activeSpace = selectedSpace?.toActiveSpace() ?: ViewRoom.ActiveSpace.Home
return ViewRoom( return ViewRoom(
isDM = isDirect, isDM = info().isDirect,
isSpace = isSpace, isSpace = info().isSpace,
trigger = trigger, trigger = trigger,
activeSpace = activeSpace, activeSpace = activeSpace,
viaKeyboard = viaKeyboard viaKeyboard = viaKeyboard
@ -27,5 +27,5 @@ fun MatrixRoom.toAnalyticsViewRoom(
} }
private fun MatrixRoom.toActiveSpace(): ViewRoom.ActiveSpace { private fun MatrixRoom.toActiveSpace(): ViewRoom.ActiveSpace {
return if (isPublic) ViewRoom.ActiveSpace.Public else ViewRoom.ActiveSpace.Private return if (info().isPublic) ViewRoom.ActiveSpace.Public else ViewRoom.ActiveSpace.Private
} }

View file

@ -8,34 +8,12 @@
package io.element.android.libraries.matrix.api.auth package io.element.android.libraries.matrix.api.auth
sealed interface OidcPrompt { sealed interface OidcPrompt {
/**
* The Authorization Server must not display any authentication or consent
* user interface pages.
*/
data object None : OidcPrompt
/** /**
* The Authorization Server should prompt the End-User for * The Authorization Server should prompt the End-User for
* reauthentication. * reauthentication.
*/ */
data object Login : OidcPrompt data object Login : OidcPrompt
/**
* The Authorization Server should prompt the End-User for consent before
* returning information to the Client.
*/
data object Consent : OidcPrompt
/**
* The Authorization Server should prompt the End-User to select a user
* account.
*
* This enables an End-User who has multiple accounts at the Authorization
* Server to select amongst the multiple accounts that they might have
* current sessions for.
*/
data object SelectAccount : OidcPrompt
/** /**
* The Authorization Server should prompt the End-User to create a user * The Authorization Server should prompt the End-User to create a user
* account. * account.

View file

@ -0,0 +1,21 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.platform
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
/**
* This service is responsible for initializing the platform-related settings of the SDK.
*/
interface InitPlatformService {
/**
* Initialize the platform-related settings of the SDK.
* @param tracingConfiguration the tracing configuration to use for logging.
*/
fun init(tracingConfiguration: TracingConfiguration)
}

View file

@ -45,21 +45,10 @@ import java.io.File
interface MatrixRoom : Closeable { interface MatrixRoom : Closeable {
val sessionId: SessionId val sessionId: SessionId
val roomId: RoomId val roomId: RoomId
val displayName: String
val canonicalAlias: RoomAlias?
val alternativeAliases: List<RoomAlias>
val topic: String?
val avatarUrl: String?
val isEncrypted: Boolean
val isSpace: Boolean
val isDirect: Boolean
val isPublic: Boolean
val activeMemberCount: Long
val joinedMemberCount: Long
val roomCoroutineScope: CoroutineScope val roomCoroutineScope: CoroutineScope
val roomInfoFlow: Flow<MatrixRoomInfo> val roomInfoFlow: StateFlow<MatrixRoomInfo>
val roomTypingMembersFlow: Flow<List<UserId>> val roomTypingMembersFlow: Flow<List<UserId>>
val identityStateChangesFlow: Flow<List<IdentityStateChange>> val identityStateChangesFlow: Flow<List<IdentityStateChange>>
@ -72,7 +61,7 @@ interface MatrixRoom : Closeable {
* A one-to-one is a room with exactly 2 members. * A one-to-one is a room with exactly 2 members.
* See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules). * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules).
*/ */
val isOneToOne: Boolean get() = activeMemberCount == 2L val isOneToOne: Boolean get() = info().activeMembersCount == 2L
/** /**
* The current loaded members as a StateFlow. * The current loaded members as a StateFlow.
@ -83,6 +72,11 @@ interface MatrixRoom : Closeable {
val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState>
/**
* Get the latest room info we have received from the SDK stream.
*/
fun info(): MatrixRoomInfo = roomInfoFlow.value
/** /**
* Try to load the room members and update the membersFlow. * Try to load the room members and update the membersFlow.
*/ */
@ -453,4 +447,6 @@ interface MatrixRoom : Closeable {
* Update the join rule for this room. * Update the join rule for this room.
*/ */
suspend fun updateJoinRule(joinRule: JoinRule): Result<Unit> suspend fun updateJoinRule(joinRule: JoinRule): Result<Unit>
suspend fun getUpdatedIsEncrypted(): Result<Boolean>
} }

View file

@ -27,7 +27,9 @@ data class MatrixRoomInfo(
val rawName: String?, val rawName: String?,
val topic: String?, val topic: String?,
val avatarUrl: String?, val avatarUrl: String?,
val isPublic: Boolean,
val isDirect: Boolean, val isDirect: Boolean,
val isEncrypted: Boolean?,
val joinRule: JoinRule?, val joinRule: JoinRule?,
val isSpace: Boolean, val isSpace: Boolean,
val isTombstoned: Boolean, val isTombstoned: Boolean,

View file

@ -7,6 +7,8 @@
package io.element.android.libraries.matrix.api.room package io.element.android.libraries.matrix.api.room
import kotlinx.coroutines.flow.first
/** /**
* Returns whether the room with the provided info is a DM. * Returns whether the room with the provided info is a DM.
* A DM is a room with at most 2 active members (one of them may have left). * A DM is a room with at most 2 active members (one of them may have left).
@ -19,9 +21,9 @@ fun isDm(isDirect: Boolean, activeMembersCount: Int): Boolean {
} }
/** /**
* Returns whether the [MatrixRoom] is a DM. * Returns whether the [MatrixRoom] is a DM, with an updated state from the latest [MatrixRoomInfo].
*/ */
val MatrixRoom.isDm get() = isDm(isDirect, activeMemberCount.toInt()) suspend fun MatrixRoom.isDm() = roomInfoFlow.first().isDm
/** /**
* Returns whether the [MatrixRoomInfo] is from a DM. * Returns whether the [MatrixRoomInfo] is from a DM.

View file

@ -19,7 +19,7 @@ fun MatrixRoom.matches(roomIdOrAlias: RoomIdOrAlias): Boolean {
roomIdOrAlias.roomId == roomId roomIdOrAlias.roomId == roomId
} }
is RoomIdOrAlias.Alias -> { is RoomIdOrAlias.Alias -> {
roomIdOrAlias.roomAlias == canonicalAlias || roomIdOrAlias.roomAlias in alternativeAliases roomIdOrAlias.roomAlias == info().canonicalAlias || roomIdOrAlias.roomAlias in info().alternativeAliases
} }
} }
} }

View file

@ -32,7 +32,7 @@ suspend fun MatrixClient.getRecentDirectRooms(
getRecentlyVisitedRooms().getOrNull()?.let { roomIds -> getRecentlyVisitedRooms().getOrNull()?.let { roomIds ->
roomIds roomIds
.mapNotNull { roomId -> getRoom(roomId) } .mapNotNull { roomId -> getRoom(roomId) }
.filter { it.isDm && it.isJoined() } .filter { it.isDm() && it.isJoined() }
.map { room -> .map { room ->
val otherUser = room.getMembers().getOrNull() val otherUser = room.getMembers().getOrNull()
?.firstOrNull { it.userId != sessionId } ?.firstOrNull { it.userId != sessionId }

View file

@ -10,5 +10,6 @@ package io.element.android.libraries.matrix.api.timeline.item.event
enum class TimelineItemEventOrigin { enum class TimelineItemEventOrigin {
LOCAL, LOCAL,
SYNC, SYNC,
PAGINATION PAGINATION,
CACHE,
} }

View file

@ -10,6 +10,5 @@ package io.element.android.libraries.matrix.api.tracing
import timber.log.Timber import timber.log.Timber
interface TracingService { interface TracingService {
fun setupTracing(tracingConfiguration: TracingConfiguration)
fun createTimberTree(target: String): Timber.Tree fun createTimberTree(target: String): Timber.Tree
} }

View file

@ -151,8 +151,7 @@ class RustMatrixClient(
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(innerSyncService) private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(innerSyncService)
private val innerNotificationClient = runBlocking { innerClient.notificationClient(notificationProcessSetup) } private val innerNotificationClient = runBlocking { innerClient.notificationClient(notificationProcessSetup) }
private val notificationService = RustNotificationService(innerNotificationClient, dispatchers, clock) private val notificationService = RustNotificationService(innerNotificationClient, dispatchers, clock)
private val notificationSettingsService = RustNotificationSettingsService(innerClient, dispatchers) private val notificationSettingsService = RustNotificationSettingsService(innerClient, sessionCoroutineScope, dispatchers)
.apply { start() }
private val encryptionService = RustEncryptionService( private val encryptionService = RustEncryptionService(
client = innerClient, client = innerClient,
syncService = rustSyncService, syncService = rustSyncService,
@ -216,7 +215,7 @@ class RustMatrixClient(
userId = sessionId, userId = sessionId,
// TODO cache for displayName? // TODO cache for displayName?
displayName = null, displayName = null,
avatarUrl = innerClient.cachedAvatarUrl(), avatarUrl = null,
) )
) )
@ -237,6 +236,9 @@ class RustMatrixClient(
sessionDelegate.bindClient(this) sessionDelegate.bindClient(this)
sessionCoroutineScope.launch { sessionCoroutineScope.launch {
// Start notification settings
notificationSettingsService.start()
// Force a refresh of the profile // Force a refresh of the profile
getUserProfile() getUserProfile()
} }
@ -479,10 +481,10 @@ class RustMatrixClient(
appCoroutineScope.launch { appCoroutineScope.launch {
roomFactory.destroy() roomFactory.destroy()
rustSyncService.destroy() rustSyncService.destroy()
notificationSettingsService.destroy()
} }
sessionCoroutineScope.cancel() sessionCoroutineScope.cancel()
clientDelegateTaskHandle?.cancelAndDestroy() clientDelegateTaskHandle?.cancelAndDestroy()
notificationSettingsService.destroy()
verificationService.destroy() verificationService.destroy()
sessionDelegate.clearCurrentClient() sessionDelegate.clearCurrentClient()

View file

@ -9,6 +9,8 @@ package io.element.android.libraries.matrix.impl.analytics
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.isDm
import kotlinx.coroutines.flow.first
private fun Long.toAnalyticsRoomSize(): JoinedRoom.RoomSize { private fun Long.toAnalyticsRoomSize(): JoinedRoom.RoomSize {
return when (this) { return when (this) {
@ -22,11 +24,12 @@ private fun Long.toAnalyticsRoomSize(): JoinedRoom.RoomSize {
} }
} }
fun MatrixRoom.toAnalyticsJoinedRoom(trigger: JoinedRoom.Trigger?): JoinedRoom { suspend fun MatrixRoom.toAnalyticsJoinedRoom(trigger: JoinedRoom.Trigger?): JoinedRoom {
val roomInfo = roomInfoFlow.first()
return JoinedRoom( return JoinedRoom(
isDM = isDirect, isDM = roomInfo.isDm,
isSpace = isSpace, isSpace = roomInfo.isSpace,
roomSize = joinedMemberCount.toAnalyticsRoomSize(), roomSize = roomInfo.joinedMembersCount.toAnalyticsRoomSize(),
trigger = trigger trigger = trigger
) )
} }

View file

@ -12,10 +12,7 @@ import org.matrix.rustcomponents.sdk.OidcPrompt as RustOidcPrompt
internal fun OidcPrompt.toRustPrompt(): RustOidcPrompt { internal fun OidcPrompt.toRustPrompt(): RustOidcPrompt {
return when (this) { return when (this) {
OidcPrompt.None -> RustOidcPrompt.None OidcPrompt.Login -> RustOidcPrompt.Unknown("consent")
OidcPrompt.Login -> RustOidcPrompt.Login
OidcPrompt.Consent -> RustOidcPrompt.Consent
OidcPrompt.SelectAccount -> RustOidcPrompt.SelectAccount
OidcPrompt.Create -> RustOidcPrompt.Create OidcPrompt.Create -> RustOidcPrompt.Create
is OidcPrompt.Unknown -> RustOidcPrompt.Unknown(value) is OidcPrompt.Unknown -> RustOidcPrompt.Unknown(value)
} }

View file

@ -8,10 +8,12 @@
package io.element.android.libraries.matrix.impl.notificationsettings package io.element.android.libraries.matrix.impl.notificationsettings
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.suspendLazy
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
@ -24,9 +26,10 @@ import timber.log.Timber
class RustNotificationSettingsService( class RustNotificationSettingsService(
client: Client, client: Client,
sessionCoroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
) : NotificationSettingsService { ) : NotificationSettingsService {
private val notificationSettings = client.getNotificationSettings() private val notificationSettings by suspendLazy(sessionCoroutineScope.coroutineContext + dispatchers.io) { client.getNotificationSettings() }
private val _notificationSettingsChangeFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val _notificationSettingsChangeFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val notificationSettingsChangeFlow: SharedFlow<Unit> = _notificationSettingsChangeFlow.asSharedFlow() override val notificationSettingsChangeFlow: SharedFlow<Unit> = _notificationSettingsChangeFlow.asSharedFlow()
@ -36,22 +39,22 @@ class RustNotificationSettingsService(
} }
} }
fun start() { suspend fun start() {
notificationSettings.setDelegate(notificationSettingsDelegate) notificationSettings.await().setDelegate(notificationSettingsDelegate)
} }
fun destroy() { suspend fun destroy() {
notificationSettings.setDelegate(null) notificationSettings.await().setDelegate(null)
} }
override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationSettings> = override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationSettings> =
runCatching { runCatching {
notificationSettings.getRoomNotificationSettings(roomId.value, isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::map) notificationSettings.await().getRoomNotificationSettings(roomId.value, isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::map)
} }
override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationMode> = override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationMode> =
runCatching { runCatching {
notificationSettings.getDefaultRoomNotificationMode(isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::mapMode) notificationSettings.await().getDefaultRoomNotificationMode(isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::mapMode)
} }
override suspend fun setDefaultRoomNotificationMode( override suspend fun setDefaultRoomNotificationMode(
@ -61,7 +64,7 @@ class RustNotificationSettingsService(
): Result<Unit> = withContext(dispatchers.io) { ): Result<Unit> = withContext(dispatchers.io) {
runCatching { runCatching {
try { try {
notificationSettings.setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode)) notificationSettings.await().setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode))
} catch (exception: NotificationSettingsException.RuleNotFound) { } catch (exception: NotificationSettingsException.RuleNotFound) {
// `setDefaultRoomNotificationMode` updates multiple rules including unstable rules (e.g. the polls push rules defined in the MSC3930) // `setDefaultRoomNotificationMode` updates multiple rules including unstable rules (e.g. the polls push rules defined in the MSC3930)
// since production home servers may not have these rules yet, we drop the RuleNotFound error // since production home servers may not have these rules yet, we drop the RuleNotFound error
@ -72,13 +75,13 @@ class RustNotificationSettingsService(
override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result<Unit> = withContext(dispatchers.io) { override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result<Unit> = withContext(dispatchers.io) {
runCatching { runCatching {
notificationSettings.setRoomNotificationMode(roomId.value, mode.let(RoomNotificationSettingsMapper::mapMode)) notificationSettings.await().setRoomNotificationMode(roomId.value, mode.let(RoomNotificationSettingsMapper::mapMode))
} }
} }
override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result<Unit> = withContext(dispatchers.io) { override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result<Unit> = withContext(dispatchers.io) {
runCatching { runCatching {
notificationSettings.restoreDefaultRoomNotificationMode(roomId.value) notificationSettings.await().restoreDefaultRoomNotificationMode(roomId.value)
} }
} }
@ -86,53 +89,53 @@ class RustNotificationSettingsService(
override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean) = withContext(dispatchers.io) { override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean) = withContext(dispatchers.io) {
runCatching { runCatching {
notificationSettings.unmuteRoom(roomId.value, isEncrypted, isOneToOne) notificationSettings.await().unmuteRoom(roomId.value, isEncrypted, isOneToOne)
} }
} }
override suspend fun isRoomMentionEnabled(): Result<Boolean> = withContext(dispatchers.io) { override suspend fun isRoomMentionEnabled(): Result<Boolean> = withContext(dispatchers.io) {
runCatching { runCatching {
notificationSettings.isRoomMentionEnabled() notificationSettings.await().isRoomMentionEnabled()
} }
} }
override suspend fun setRoomMentionEnabled(enabled: Boolean): Result<Unit> = withContext(dispatchers.io) { override suspend fun setRoomMentionEnabled(enabled: Boolean): Result<Unit> = withContext(dispatchers.io) {
runCatching { runCatching {
notificationSettings.setRoomMentionEnabled(enabled) notificationSettings.await().setRoomMentionEnabled(enabled)
} }
} }
override suspend fun isCallEnabled(): Result<Boolean> = withContext(dispatchers.io) { override suspend fun isCallEnabled(): Result<Boolean> = withContext(dispatchers.io) {
runCatching { runCatching {
notificationSettings.isCallEnabled() notificationSettings.await().isCallEnabled()
} }
} }
override suspend fun setCallEnabled(enabled: Boolean): Result<Unit> = withContext(dispatchers.io) { override suspend fun setCallEnabled(enabled: Boolean): Result<Unit> = withContext(dispatchers.io) {
runCatching { runCatching {
notificationSettings.setCallEnabled(enabled) notificationSettings.await().setCallEnabled(enabled)
} }
} }
override suspend fun isInviteForMeEnabled(): Result<Boolean> = withContext(dispatchers.io) { override suspend fun isInviteForMeEnabled(): Result<Boolean> = withContext(dispatchers.io) {
runCatching { runCatching {
notificationSettings.isInviteForMeEnabled() notificationSettings.await().isInviteForMeEnabled()
} }
} }
override suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit> = withContext(dispatchers.io) { override suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit> = withContext(dispatchers.io) {
runCatching { runCatching {
notificationSettings.setInviteForMeEnabled(enabled) notificationSettings.await().setInviteForMeEnabled(enabled)
} }
} }
override suspend fun getRoomsWithUserDefinedRules(): Result<List<String>> = override suspend fun getRoomsWithUserDefinedRules(): Result<List<String>> =
runCatching { runCatching {
notificationSettings.getRoomsWithUserDefinedRules(enabled = true) notificationSettings.await().getRoomsWithUserDefinedRules(enabled = true)
} }
override suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean> = override suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean> =
runCatching { runCatching {
notificationSettings.canPushEncryptedEventToDevice() notificationSettings.await().canPushEncryptedEventToDevice()
} }
} }

View file

@ -0,0 +1,26 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.platform
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.platform.InitPlatformService
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.libraries.matrix.impl.tracing.map
import org.matrix.rustcomponents.sdk.initPlatform
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class RustInitPlatformService @Inject constructor() : InitPlatformService {
override fun init(tracingConfiguration: TracingConfiguration) {
initPlatform(
config = tracingConfiguration.map(),
useLightweightTokioRuntime = false
)
}
}

View file

@ -23,6 +23,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap import kotlinx.collections.immutable.toPersistentMap
import org.matrix.rustcomponents.sdk.Membership import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.RoomHero import org.matrix.rustcomponents.sdk.RoomHero
import uniffi.matrix_sdk_base.EncryptionState
import org.matrix.rustcomponents.sdk.Membership as RustMembership import org.matrix.rustcomponents.sdk.Membership as RustMembership
import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo
import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode
@ -36,7 +37,13 @@ class MatrixRoomInfoMapper {
rawName = it.rawName, rawName = it.rawName,
topic = it.topic, topic = it.topic,
avatarUrl = it.avatarUrl, avatarUrl = it.avatarUrl,
isPublic = it.isPublic,
isDirect = it.isDirect, isDirect = it.isDirect,
isEncrypted = when (it.encryptionState) {
EncryptionState.ENCRYPTED -> true
EncryptionState.NOT_ENCRYPTED -> false
EncryptionState.UNKNOWN -> null
},
joinRule = it.joinRule?.map(), joinRule = it.joinRule?.map(),
isSpace = it.isSpace, isSpace = it.isSpace,
isTombstoned = it.isTombstoned, isTombstoned = it.isTombstoned,
@ -63,6 +70,44 @@ class MatrixRoomInfoMapper {
historyVisibility = it.historyVisibility.map(), historyVisibility = it.historyVisibility.map(),
) )
} }
// fun map(rustRoom: Room): MatrixRoomInfo = with(rustRoom) {
// return MatrixRoomInfo(
// id = RoomId(id()),
// name = rawName(),
// rawName = displayName(),
// topic = topic(),
// avatarUrl = avatarUrl(),
// isPublic = isPublic(),
// isDirect = null,
// isEncrypted = encryptionState() == EncryptionState.ENCRYPTED,
// joinRule = null,
// isSpace = isSpace(),
// isTombstoned = isTombstoned(),
// isFavorite = null,
// canonicalAlias = canonicalAlias()?.let(::RoomAlias),
// alternativeAliases = alternativeAliases().map(::RoomAlias).toImmutableList(),
// currentUserMembership = membership().map(),
// inviter = null,
// activeMembersCount = activeMembersCount().toLong(),
// invitedMembersCount = invitedMembersCount().toLong(),
// joinedMembersCount = joinedMembersCount().toLong(),
// userPowerLevels = persistentMapOf(),
// highlightCount = 0,
// notificationCount = 0,
// userDefinedNotificationMode = null,
// hasRoomCall = hasActiveRoomCall(),
// activeRoomCallParticipants = activeRoomCallParticipants().map(::UserId).toImmutableList(),
// isMarkedUnread = false,
// numUnreadMessages = 0,
// numUnreadNotifications = 0,
// numUnreadMentions = 0,
// heroes = heroes().map(RoomHero::map).toImmutableList(),
// pinnedEventIds = persistentListOf(),
// creator = null,
// historyVisibility = null,
// )
// }
} }
fun RustMembership.map(): CurrentUserMembership = when (this) { fun RustMembership.map(): CurrentUserMembership = when (this) {

View file

@ -73,6 +73,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -82,13 +83,13 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.DateDividerMode import org.matrix.rustcomponents.sdk.DateDividerMode
import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener
import org.matrix.rustcomponents.sdk.KnockRequestsListener import org.matrix.rustcomponents.sdk.KnockRequestsListener
import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType
import org.matrix.rustcomponents.sdk.TimelineConfiguration import org.matrix.rustcomponents.sdk.TimelineConfiguration
import org.matrix.rustcomponents.sdk.TimelineFilter import org.matrix.rustcomponents.sdk.TimelineFilter
@ -101,6 +102,7 @@ import org.matrix.rustcomponents.sdk.getElementCallRequiredPermissions
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import uniffi.matrix_sdk.RoomPowerLevelChanges import uniffi.matrix_sdk.RoomPowerLevelChanges
import uniffi.matrix_sdk_base.EncryptionState
import java.io.File import java.io.File
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange
@ -112,7 +114,6 @@ import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
class RustMatrixRoom( class RustMatrixRoom(
override val sessionId: SessionId, override val sessionId: SessionId,
private val deviceId: DeviceId, private val deviceId: DeviceId,
private val roomListItem: RoomListItem,
private val innerRoom: InnerRoom, private val innerRoom: InnerRoom,
innerTimeline: InnerTimeline, innerTimeline: InnerTimeline,
private val notificationSettingsService: NotificationSettingsService, private val notificationSettingsService: NotificationSettingsService,
@ -124,22 +125,17 @@ class RustMatrixRoom(
private val matrixRoomInfoMapper: MatrixRoomInfoMapper, private val matrixRoomInfoMapper: MatrixRoomInfoMapper,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val roomMembershipObserver: RoomMembershipObserver, private val roomMembershipObserver: RoomMembershipObserver,
initialRoomInfo: MatrixRoomInfo,
) : MatrixRoom { ) : MatrixRoom {
override val roomId = RoomId(innerRoom.id()) override val roomId = RoomId(innerRoom.id())
override val roomInfoFlow: Flow<MatrixRoomInfo> = mxCallbackFlow { override val roomInfoFlow: StateFlow<MatrixRoomInfo> = mxCallbackFlow {
runCatching { innerRoom.roomInfo() }
.getOrNull()
?.let(matrixRoomInfoMapper::map)
?.let { initial ->
channel.trySend(initial)
}
innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener { innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener {
override fun call(roomInfo: RoomInfo) { override fun call(roomInfo: RoomInfo) {
channel.trySend(matrixRoomInfoMapper.map(roomInfo)) channel.trySend(matrixRoomInfoMapper.map(roomInfo))
} }
}) })
} }.stateIn(sessionCoroutineScope, started = SharingStarted.Lazily, initialValue = initialRoomInfo)
override val roomTypingMembersFlow: Flow<List<UserId>> = mxCallbackFlow { override val roomTypingMembersFlow: Flow<List<UserId>> = mxCallbackFlow {
val initial = emptyList<UserId>() val initial = emptyList<UserId>()
@ -307,39 +303,6 @@ class RustMatrixRoom(
liveTimeline.close() liveTimeline.close()
} }
override val displayName: String
get() = runCatching { innerRoom.displayName() ?: "" }.getOrDefault("")
override val topic: String?
get() = runCatching { innerRoom.topic() }.getOrDefault(null)
override val avatarUrl: String?
get() = runCatching { roomListItem.avatarUrl() ?: innerRoom.avatarUrl() }.getOrDefault(null)
override val isEncrypted: Boolean
get() = runCatching { innerRoom.isEncrypted() }.getOrDefault(false)
override val canonicalAlias: RoomAlias?
get() = runCatching { innerRoom.canonicalAlias()?.let(::RoomAlias) }.getOrDefault(null)
override val alternativeAliases: List<RoomAlias>
get() = runCatching { innerRoom.alternativeAliases().map { RoomAlias(it) } }.getOrDefault(emptyList())
override val isPublic: Boolean
get() = runCatching { innerRoom.isPublic() }.getOrDefault(false)
override val isSpace: Boolean
get() = runCatching { innerRoom.isSpace() }.getOrDefault(false)
override val isDirect: Boolean
get() = runCatching { innerRoom.isDirect() }.getOrDefault(false)
override val joinedMemberCount: Long
get() = runCatching { innerRoom.joinedMembersCount().toLong() }.getOrDefault(0)
override val activeMemberCount: Long
get() = runCatching { innerRoom.activeMembersCount().toLong() }.getOrDefault(0)
override suspend fun updateMembers() { override suspend fun updateMembers() {
val useCache = membersStateFlow.value is MatrixRoomMembersState.Unknown val useCache = membersStateFlow.value is MatrixRoomMembersState.Unknown
val source = if (useCache) { val source = if (useCache) {
@ -377,6 +340,7 @@ class RustMatrixRoom(
val currentRoomNotificationSettings = currentState.roomNotificationSettings() val currentRoomNotificationSettings = currentState.roomNotificationSettings()
_roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings) _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings)
runCatching { runCatching {
val isEncrypted = roomInfoFlow.value.isEncrypted ?: getUpdatedIsEncrypted().getOrThrow()
notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow()
}.map { }.map {
_roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(it) _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(it)
@ -866,6 +830,12 @@ class RustMatrixRoom(
} }
} }
override suspend fun getUpdatedIsEncrypted(): Result<Boolean> = withContext(roomDispatcher) {
runCatching {
innerRoom.latestEncryptionState() == EncryptionState.ENCRYPTED
}
}
private fun createTimeline( private fun createTimeline(
timeline: InnerTimeline, timeline: InnerTimeline,
mode: Timeline.Mode, mode: Timeline.Mode,

View file

@ -104,10 +104,10 @@ class RustRoomFactory(
return@withContext null return@withContext null
} }
val liveTimeline = roomReferences.fullRoom.timeline() val liveTimeline = roomReferences.fullRoom.timeline()
val initialRoomInfo = roomReferences.fullRoom.roomInfo()
RustMatrixRoom( RustMatrixRoom(
sessionId = sessionId, sessionId = sessionId,
deviceId = deviceId, deviceId = deviceId,
roomListItem = roomReferences.roomListItem,
innerRoom = roomReferences.fullRoom, innerRoom = roomReferences.fullRoom,
innerTimeline = liveTimeline, innerTimeline = liveTimeline,
sessionCoroutineScope = sessionCoroutineScope, sessionCoroutineScope = sessionCoroutineScope,
@ -119,6 +119,7 @@ class RustRoomFactory(
matrixRoomInfoMapper = matrixRoomInfoMapper, matrixRoomInfoMapper = matrixRoomInfoMapper,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
roomMembershipObserver = roomMembershipObserver, roomMembershipObserver = roomMembershipObserver,
initialRoomInfo = matrixRoomInfoMapper.map(initialRoomInfo),
) )
} }
} }

View file

@ -56,6 +56,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -214,19 +215,19 @@ class RustTimeline(
_timelineItems, _timelineItems,
backPaginationStatus, backPaginationStatus,
forwardPaginationStatus, forwardPaginationStatus,
matrixRoom.roomInfoFlow.map { it.creator }, matrixRoom.roomInfoFlow.map { it.creator to it.isDm }.distinctUntilChanged(),
isTimelineInitialized, isTimelineInitialized,
) { timelineItems, ) { timelineItems,
backwardPaginationStatus, backwardPaginationStatus,
forwardPaginationStatus, forwardPaginationStatus,
roomCreator, (roomCreator, isDm),
isTimelineInitialized -> isTimelineInitialized ->
withContext(dispatcher) { withContext(dispatcher) {
timelineItems timelineItems
.let { items -> .let { items ->
roomBeginningPostProcessor.process( roomBeginningPostProcessor.process(
items = items, items = items,
isDm = matrixRoom.isDm, isDm = isDm,
roomCreator = roomCreator, roomCreator = roomCreator,
hasMoreToLoadBackwards = backwardPaginationStatus.hasMoreToLoad, hasMoreToLoadBackwards = backwardPaginationStatus.hasMoreToLoad,
) )

View file

@ -152,6 +152,7 @@ private fun RustEventItemOrigin.map(): TimelineItemEventOrigin {
RustEventItemOrigin.LOCAL -> TimelineItemEventOrigin.LOCAL RustEventItemOrigin.LOCAL -> TimelineItemEventOrigin.LOCAL
RustEventItemOrigin.SYNC -> TimelineItemEventOrigin.SYNC RustEventItemOrigin.SYNC -> TimelineItemEventOrigin.SYNC
RustEventItemOrigin.PAGINATION -> TimelineItemEventOrigin.PAGINATION RustEventItemOrigin.PAGINATION -> TimelineItemEventOrigin.PAGINATION
RustEventItemOrigin.CACHE -> TimelineItemEventOrigin.CACHE
} }
} }

View file

@ -20,17 +20,6 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class RustTracingService @Inject constructor(private val buildMeta: BuildMeta) : TracingService { class RustTracingService @Inject constructor(private val buildMeta: BuildMeta) : TracingService {
override fun setupTracing(tracingConfiguration: TracingConfiguration) {
val rustTracingConfiguration = org.matrix.rustcomponents.sdk.TracingConfiguration(
writeToStdoutOrSystem = tracingConfiguration.writesToLogcat,
logLevel = tracingConfiguration.logLevel.toRustLogLevel(),
extraTargets = tracingConfiguration.extraTargets,
writeToFiles = tracingConfiguration.writesToFilesConfiguration.toTracingFileConfiguration(),
)
org.matrix.rustcomponents.sdk.setupTracing(rustTracingConfiguration)
Timber.d("setupTracing: $rustTracingConfiguration")
}
override fun createTimberTree(target: String): Timber.Tree { override fun createTimberTree(target: String): Timber.Tree {
return RustTracingTree(target = target, retrieveFromStackTrace = buildMeta.isDebuggable) return RustTracingTree(target = target, retrieveFromStackTrace = buildMeta.isDebuggable)
} }
@ -57,3 +46,10 @@ private fun WriteToFilesConfiguration.toTracingFileConfiguration(): TracingFileC
) )
} }
} }
fun TracingConfiguration.map(): org.matrix.rustcomponents.sdk.TracingConfiguration = org.matrix.rustcomponents.sdk.TracingConfiguration(
writeToStdoutOrSystem = writesToLogcat,
logLevel = logLevel.toRustLogLevel(),
extraTargets = extraTargets,
writeToFiles = writesToFilesConfiguration.toTracingFileConfiguration(),
)

View file

@ -42,7 +42,7 @@ fun TestScope.createRustMatrixClientFactory(
) = RustMatrixClientFactory( ) = RustMatrixClientFactory(
baseDirectory = baseDirectory, baseDirectory = baseDirectory,
cacheDirectory = cacheDirectory, cacheDirectory = cacheDirectory,
appCoroutineScope = this, appCoroutineScope = backgroundScope,
coroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers = testCoroutineDispatchers(),
sessionStore = sessionStore, sessionStore = sessionStore,
userAgentProvider = SimpleUserAgentProvider(), userAgentProvider = SimpleUserAgentProvider(),

View file

@ -38,7 +38,7 @@ class RustMatrixClientTest {
innerClient = FakeRustClient(), innerClient = FakeRustClient(),
baseDirectory = File(""), baseDirectory = File(""),
sessionStore = sessionStore, sessionStore = sessionStore,
appCoroutineScope = this, appCoroutineScope = backgroundScope,
sessionDelegate = aRustClientSessionDelegate( sessionDelegate = aRustClientSessionDelegate(
sessionStore = sessionStore, sessionStore = sessionStore,
), ),

View file

@ -9,13 +9,14 @@ package io.element.android.libraries.matrix.impl.analytics
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
class JoinedRoomExtKtTest { class JoinedRoomExtKtTest {
@Test @Test
fun `test room size mapping`() { fun `test room size mapping`() = runTest {
mapOf( mapOf(
listOf(0L, 1L) to JoinedRoom.RoomSize.One, listOf(0L, 1L) to JoinedRoom.RoomSize.One,
listOf(2L, 2L) to JoinedRoom.RoomSize.Two, listOf(2L, 2L) to JoinedRoom.RoomSize.Two,
@ -39,7 +40,7 @@ class JoinedRoomExtKtTest {
} }
@Test @Test
fun `test isDirect parameter mapping`() { fun `test isDirect parameter mapping`() = runTest {
assertThat(aMatrixRoom(isDirect = true).toAnalyticsJoinedRoom(null)) assertThat(aMatrixRoom(isDirect = true).toAnalyticsJoinedRoom(null))
.isEqualTo( .isEqualTo(
JoinedRoom( JoinedRoom(
@ -52,7 +53,7 @@ class JoinedRoomExtKtTest {
} }
@Test @Test
fun `test isSpace parameter mapping`() { fun `test isSpace parameter mapping`() = runTest {
assertThat(aMatrixRoom(isSpace = true).toAnalyticsJoinedRoom(null)) assertThat(aMatrixRoom(isSpace = true).toAnalyticsJoinedRoom(null))
.isEqualTo( .isEqualTo(
JoinedRoom( JoinedRoom(
@ -65,8 +66,8 @@ class JoinedRoomExtKtTest {
} }
@Test @Test
fun `test trigger parameter mapping`() { fun `test trigger parameter mapping`() = runTest {
assertThat(aMatrixRoom().toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) assertThat(aMatrixRoom(isDirect = false, isSpace = false, joinedMemberCount = 1).toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
.isEqualTo( .isEqualTo(
JoinedRoom( JoinedRoom(
isDM = false, isDM = false,
@ -81,11 +82,9 @@ class JoinedRoomExtKtTest {
isDirect: Boolean = false, isDirect: Boolean = false,
isSpace: Boolean = false, isSpace: Boolean = false,
joinedMemberCount: Long = 0 joinedMemberCount: Long = 0
): MatrixRoom { ): FakeMatrixRoom {
return FakeMatrixRoom( return FakeMatrixRoom().apply {
isDirect = isDirect, givenRoomInfo(aRoomInfo(isDirect = isDirect, isSpace = isSpace, joinedMembersCount = joinedMemberCount))
isSpace = isSpace, }
joinedMemberCount = joinedMemberCount
)
} }
} }

View file

@ -17,6 +17,7 @@ import org.matrix.rustcomponents.sdk.RoomHistoryVisibility
import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomNotificationMode import org.matrix.rustcomponents.sdk.RoomNotificationMode
import uniffi.matrix_sdk_base.EncryptionState
fun aRustRoomInfo( fun aRustRoomInfo(
id: String = A_ROOM_ID.value, id: String = A_ROOM_ID.value,
@ -24,6 +25,7 @@ fun aRustRoomInfo(
rawName: String? = A_ROOM_NAME, rawName: String? = A_ROOM_NAME,
topic: String? = null, topic: String? = null,
avatarUrl: String? = null, avatarUrl: String? = null,
encryptionState: EncryptionState = EncryptionState.UNKNOWN,
isDirect: Boolean = false, isDirect: Boolean = false,
isPublic: Boolean = false, isPublic: Boolean = false,
isSpace: Boolean = false, isSpace: Boolean = false,
@ -57,6 +59,7 @@ fun aRustRoomInfo(
rawName = rawName, rawName = rawName,
topic = topic, topic = topic,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,
encryptionState = encryptionState,
isDirect = isDirect, isDirect = isDirect,
isPublic = isPublic, isPublic = isPublic,
isSpace = isSpace, isSpace = isSpace,

View file

@ -35,11 +35,11 @@ class FakeRustClient(
override fun userId(): String = userId override fun userId(): String = userId
override fun deviceId(): String = deviceId override fun deviceId(): String = deviceId
override suspend fun notificationClient(processSetup: NotificationProcessSetup) = notificationClient override suspend fun notificationClient(processSetup: NotificationProcessSetup) = notificationClient
override fun getNotificationSettings(): NotificationSettings = notificationSettings override suspend fun getNotificationSettings(): NotificationSettings = notificationSettings
override fun encryption(): Encryption = encryption override fun encryption(): Encryption = encryption
override fun session(): Session = session override fun session(): Session = session
override fun setDelegate(delegate: ClientDelegate?): TaskHandle = FakeRustTaskHandle() override fun setDelegate(delegate: ClientDelegate?): TaskHandle = FakeRustTaskHandle()
override fun cachedAvatarUrl(): String? = null override suspend fun cachedAvatarUrl(): String? = null
override suspend fun restoreSession(session: Session) = Unit override suspend fun restoreSession(session: Session) = Unit
override fun syncService(): SyncServiceBuilder = FakeRustSyncServiceBuilder() override fun syncService(): SyncServiceBuilder = FakeRustSyncServiceBuilder()
override fun roomDirectorySearch(): RoomDirectorySearch = FakeRustRoomDirectorySearch() override fun roomDirectorySearch(): RoomDirectorySearch = FakeRustRoomDirectorySearch()

View file

@ -37,6 +37,7 @@ class RustNotificationSettingsServiceTest {
client = FakeRustClient( client = FakeRustClient(
notificationSettings = notificationSettings, notificationSettings = notificationSettings,
), ),
sessionCoroutineScope = this,
dispatchers = testCoroutineDispatchers(), dispatchers = testCoroutineDispatchers(),
) )
} }

View file

@ -33,6 +33,7 @@ import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import org.junit.Test import org.junit.Test
import org.matrix.rustcomponents.sdk.Membership import org.matrix.rustcomponents.sdk.Membership
import uniffi.matrix_sdk_base.EncryptionState
import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule
import org.matrix.rustcomponents.sdk.RoomHistoryVisibility as RustRoomHistoryVisibility import org.matrix.rustcomponents.sdk.RoomHistoryVisibility as RustRoomHistoryVisibility
import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode
@ -48,6 +49,7 @@ class MatrixRoomInfoMapperTest {
rawName = "rawName", rawName = "rawName",
topic = "topic", topic = "topic",
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
encryptionState = EncryptionState.ENCRYPTED,
isDirect = true, isDirect = true,
isPublic = false, isPublic = false,
isSpace = false, isSpace = false,
@ -84,7 +86,9 @@ class MatrixRoomInfoMapperTest {
rawName = "rawName", rawName = "rawName",
topic = "topic", topic = "topic",
avatarUrl = AN_AVATAR_URL, avatarUrl = AN_AVATAR_URL,
isPublic = false,
isDirect = true, isDirect = true,
isEncrypted = true,
isSpace = false, isSpace = false,
isTombstoned = false, isTombstoned = false,
isFavorite = false, isFavorite = false,
@ -130,6 +134,7 @@ class MatrixRoomInfoMapperTest {
rawName = null, rawName = null,
topic = null, topic = null,
avatarUrl = null, avatarUrl = null,
encryptionState = EncryptionState.UNKNOWN,
isDirect = false, isDirect = false,
isPublic = true, isPublic = true,
joinRule = null, joinRule = null,
@ -165,6 +170,8 @@ class MatrixRoomInfoMapperTest {
rawName = null, rawName = null,
topic = null, topic = null,
avatarUrl = null, avatarUrl = null,
isEncrypted = null,
isPublic = true,
isDirect = false, isDirect = false,
joinRule = null, joinRule = null,
isSpace = false, isSpace = false,

View file

@ -19,6 +19,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SERVER_LIST import io.element.android.libraries.matrix.test.A_SERVER_LIST
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -32,7 +33,9 @@ class DefaultJoinRoomTest {
val roomSummary = aRoomSummary() val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) } val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom() val roomResult = FakeMatrixRoom().apply {
givenRoomInfo(aRoomInfo())
}
val aTrigger = JoinedRoom.Trigger.MobilePermalink val aTrigger = JoinedRoom.Trigger.MobilePermalink
val client: MatrixClient = FakeMatrixClient().also { val client: MatrixClient = FakeMatrixClient().also {
it.joinRoomLambda = joinRoomLambda it.joinRoomLambda = joinRoomLambda
@ -67,7 +70,9 @@ class DefaultJoinRoomTest {
val roomSummary = aRoomSummary() val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) } val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom() val roomResult = FakeMatrixRoom().apply {
givenRoomInfo(aRoomInfo())
}
val aTrigger = JoinedRoom.Trigger.MobilePermalink val aTrigger = JoinedRoom.Trigger.MobilePermalink
val client: MatrixClient = FakeMatrixClient().also { val client: MatrixClient = FakeMatrixClient().also {
it.joinRoomLambda = joinRoomLambda it.joinRoomLambda = joinRoomLambda
@ -103,7 +108,9 @@ class DefaultJoinRoomTest {
val roomSummary = aRoomSummary() val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) } val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom() val roomResult = FakeMatrixRoom().apply {
givenRoomInfo(aRoomInfo())
}
val aTrigger = JoinedRoom.Trigger.MobilePermalink val aTrigger = JoinedRoom.Trigger.MobilePermalink
val client: MatrixClient = FakeMatrixClient().also { val client: MatrixClient = FakeMatrixClient().also {
it.joinRoomLambda = joinRoomLambda it.joinRoomLambda = joinRoomLambda

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.test.room package io.element.android.libraries.matrix.test.room
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.ProgressCallback
@ -66,19 +67,9 @@ import java.io.File
class FakeMatrixRoom( class FakeMatrixRoom(
override val sessionId: SessionId = A_SESSION_ID, override val sessionId: SessionId = A_SESSION_ID,
override val roomId: RoomId = A_ROOM_ID, override val roomId: RoomId = A_ROOM_ID,
override val displayName: String = "",
override val topic: String? = null,
override val avatarUrl: String? = null,
override var isEncrypted: Boolean = false,
override val canonicalAlias: RoomAlias? = null,
override val alternativeAliases: List<RoomAlias> = emptyList(),
override val isPublic: Boolean = true,
override val isSpace: Boolean = false,
override val isDirect: Boolean = false,
override val joinedMemberCount: Long = 123L,
override val activeMemberCount: Long = 234L,
val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
override val liveTimeline: Timeline = FakeTimeline(), override val liveTimeline: Timeline = FakeTimeline(),
initialRoomInfo: MatrixRoomInfo = aRoomInfo(),
override val roomCoroutineScope: CoroutineScope = TestScope(), override val roomCoroutineScope: CoroutineScope = TestScope(),
private var roomPermalinkResult: () -> Result<String> = { lambdaError() }, private var roomPermalinkResult: () -> Result<String> = { lambdaError() },
private var eventPermalinkResult: (EventId) -> Result<String> = { lambdaError() }, private var eventPermalinkResult: (EventId) -> Result<String> = { lambdaError() },
@ -156,8 +147,8 @@ class FakeMatrixRoom(
private val enableEncryptionResult: () -> Result<Unit> = { lambdaError() }, private val enableEncryptionResult: () -> Result<Unit> = { lambdaError() },
private val updateJoinRuleResult: (JoinRule) -> Result<Unit> = { lambdaError() }, private val updateJoinRuleResult: (JoinRule) -> Result<Unit> = { lambdaError() },
) : MatrixRoom { ) : MatrixRoom {
private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableSharedFlow(replay = 1) private val _roomInfoFlow: MutableStateFlow<MatrixRoomInfo> = MutableStateFlow(initialRoomInfo)
override val roomInfoFlow: Flow<MatrixRoomInfo> = _roomInfoFlow override val roomInfoFlow: StateFlow<MatrixRoomInfo> = _roomInfoFlow
fun givenRoomInfo(roomInfo: MatrixRoomInfo) { fun givenRoomInfo(roomInfo: MatrixRoomInfo) {
_roomInfoFlow.tryEmit(roomInfo) _roomInfoFlow.tryEmit(roomInfo)
@ -200,14 +191,14 @@ class FakeMatrixRoom(
} }
override suspend fun updateRoomNotificationSettings(): Result<Unit> = simulateLongTask { override suspend fun updateRoomNotificationSettings(): Result<Unit> = simulateLongTask {
val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, info().isEncrypted.orFalse(), isOneToOne).getOrThrow()
roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(notificationSettings) roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(notificationSettings)
return Result.success(Unit) return Result.success(Unit)
} }
override suspend fun enableEncryption(): Result<Unit> = simulateLongTask { override suspend fun enableEncryption(): Result<Unit> = simulateLongTask {
enableEncryptionResult().onSuccess { enableEncryptionResult().onSuccess {
isEncrypted = true givenRoomInfo(info().copy(isEncrypted = true))
emitSyncUpdate() emitSyncUpdate()
} }
} }
@ -616,6 +607,10 @@ class FakeMatrixRoom(
updateJoinRuleResult(joinRule) updateJoinRuleResult(joinRule)
} }
override suspend fun getUpdatedIsEncrypted(): Result<Boolean> = simulateLongTask {
Result.success(info().isEncrypted.orFalse())
}
fun givenRoomMembersState(state: MatrixRoomMembersState) { fun givenRoomMembersState(state: MatrixRoomMembersState) {
membersStateFlow.value = state membersStateFlow.value = state
} }

View file

@ -33,7 +33,9 @@ fun aRoomInfo(
rawName: String? = A_ROOM_RAW_NAME, rawName: String? = A_ROOM_RAW_NAME,
topic: String? = A_ROOM_TOPIC, topic: String? = A_ROOM_TOPIC,
avatarUrl: String? = AN_AVATAR_URL, avatarUrl: String? = AN_AVATAR_URL,
isPublic: Boolean = true,
isDirect: Boolean = false, isDirect: Boolean = false,
isEncrypted: Boolean = false,
joinRule: JoinRule? = JoinRule.Public, joinRule: JoinRule? = JoinRule.Public,
isSpace: Boolean = false, isSpace: Boolean = false,
isTombstoned: Boolean = false, isTombstoned: Boolean = false,
@ -65,7 +67,9 @@ fun aRoomInfo(
rawName = rawName, rawName = rawName,
topic = topic, topic = topic,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,
isPublic = isPublic,
isDirect = isDirect, isDirect = isDirect,
isEncrypted = isEncrypted,
joinRule = joinRule, joinRule = joinRule,
isSpace = isSpace, isSpace = isSpace,
isTombstoned = isTombstoned, isTombstoned = isTombstoned,

View file

@ -46,7 +46,9 @@ fun aRoomSummary(
rawName: String? = A_ROOM_RAW_NAME, rawName: String? = A_ROOM_RAW_NAME,
topic: String? = A_ROOM_TOPIC, topic: String? = A_ROOM_TOPIC,
avatarUrl: String? = null, avatarUrl: String? = null,
isPublic: Boolean = true,
isDirect: Boolean = false, isDirect: Boolean = false,
isEncrypted: Boolean = false,
joinRule: JoinRule? = JoinRule.Public, joinRule: JoinRule? = JoinRule.Public,
isSpace: Boolean = false, isSpace: Boolean = false,
isTombstoned: Boolean = false, isTombstoned: Boolean = false,
@ -80,7 +82,9 @@ fun aRoomSummary(
rawName = rawName, rawName = rawName,
topic = topic, topic = topic,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,
isPublic = isPublic,
isDirect = isDirect, isDirect = isDirect,
isEncrypted = isEncrypted,
joinRule = joinRule, joinRule = joinRule,
isSpace = isSpace, isSpace = isSpace,
isTombstoned = isTombstoned, isTombstoned = isTombstoned,

View file

@ -40,11 +40,12 @@ fun getRoomMemberAsState(roomMembersState: MatrixRoomMembersState, userId: UserI
@Composable @Composable
fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): State<RoomMember?> { fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): State<RoomMember?> {
val roomMembers = roomMembersState.roomMembers() val roomMembers = roomMembersState.roomMembers()
return remember(roomMembersState) { val roomInfo by roomInfoFlow.collectAsState()
return remember(roomMembersState, roomInfo.isDirect) {
derivedStateOf { derivedStateOf {
roomMembers roomMembers
?.filter { it.membership.isActive() } ?.filter { it.membership.isActive() }
?.takeIf { it.size == 2 && isDirect } ?.takeIf { it.size == 2 && roomInfo.isDirect == true }
?.find { it.userId != sessionId } ?.find { it.userId != sessionId }
} }
} }

View file

@ -67,9 +67,9 @@ fun MatrixRoom.canPinUnpin(updateKey: Long): State<Boolean> {
} }
@Composable @Composable
fun MatrixRoom.isDmAsState(updateKey: Long): State<Boolean> { fun MatrixRoom.isDmAsState(): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) { return produceState(initialValue = false) {
value = isDm roomInfoFlow.collect { value = it.isDm }
} }
} }
@ -105,25 +105,25 @@ fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State<Long> {
@Composable @Composable
fun MatrixRoom.isOwnUserAdmin(): Boolean { fun MatrixRoom.isOwnUserAdmin(): Boolean {
val roomInfo by roomInfoFlow.collectAsState(initial = null) val roomInfo by roomInfoFlow.collectAsState()
val powerLevel = roomInfo?.userPowerLevels?.get(sessionId) ?: 0L val powerLevel = roomInfo.userPowerLevels[sessionId] ?: 0L
return RoomMember.Role.forPowerLevel(powerLevel) == RoomMember.Role.ADMIN return RoomMember.Role.forPowerLevel(powerLevel) == RoomMember.Role.ADMIN
} }
@Composable @Composable
fun MatrixRoom.rawName(): String? { fun MatrixRoom.rawName(): String? {
val roomInfo by roomInfoFlow.collectAsState(initial = null) val roomInfo by roomInfoFlow.collectAsState()
return roomInfo?.rawName return roomInfo.rawName
} }
@Composable @Composable
fun MatrixRoom.topic(): String? { fun MatrixRoom.topic(): String? {
val roomInfo by roomInfoFlow.collectAsState(initial = null) val roomInfo by roomInfoFlow.collectAsState()
return roomInfo?.topic return roomInfo.topic
} }
@Composable @Composable
fun MatrixRoom.avatarUrl(): String? { fun MatrixRoom.avatarUrl(): String? {
val roomInfo by roomInfoFlow.collectAsState(initial = null) val roomInfo by roomInfoFlow.collectAsState()
return roomInfo?.avatarUrl return roomInfo.avatarUrl
} }

View file

@ -30,10 +30,10 @@ import kotlinx.coroutines.flow.onEach
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun MatrixRoom.roomMemberIdentityStateChange(): Flow<ImmutableList<RoomMemberIdentityStateChange>> { fun MatrixRoom.roomMemberIdentityStateChange(): Flow<ImmutableList<RoomMemberIdentityStateChange>> {
return syncUpdateFlow return roomInfoFlow
.filter { .filter {
// Room cannot become unencrypted, so we can just apply a filter here. // Room cannot become unencrypted, so we can just apply a filter here.
isEncrypted it.isEncrypted == true
} }
.distinctUntilChanged() .distinctUntilChanged()
.flatMapLatest { .flatMapLatest {

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID_3 import io.element.android.libraries.matrix.test.A_USER_ID_3
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -31,8 +32,10 @@ class MatrixRoomMembersTest {
fun `getDirectRoomMember emits other member for encrypted DM with 2 joined members`() = runTest { fun `getDirectRoomMember emits other member for encrypted DM with 2 joined members`() = runTest {
val matrixRoom = FakeMatrixRoom( val matrixRoom = FakeMatrixRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
isEncrypted = true, initialRoomInfo = aRoomInfo(
isDirect = true, isDirect = true,
joinedMembersCount = 2,
)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember( matrixRoom.getDirectRoomMember(
@ -47,8 +50,7 @@ class MatrixRoomMembersTest {
fun `getDirectRoomMember emit null if the room is not a dm`() = runTest { fun `getDirectRoomMember emit null if the room is not a dm`() = runTest {
val matrixRoom = FakeMatrixRoom( val matrixRoom = FakeMatrixRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
isEncrypted = true, initialRoomInfo = aRoomInfo(isDirect = false)
isDirect = false,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember( matrixRoom.getDirectRoomMember(
@ -63,8 +65,10 @@ class MatrixRoomMembersTest {
fun `getDirectRoomMember emits other member even if the room is not encrypted`() = runTest { fun `getDirectRoomMember emits other member even if the room is not encrypted`() = runTest {
val matrixRoom = FakeMatrixRoom( val matrixRoom = FakeMatrixRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
isEncrypted = false, initialRoomInfo = aRoomInfo(
isDirect = true, isDirect = true,
activeMembersCount = 2,
)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember( matrixRoom.getDirectRoomMember(
@ -79,8 +83,7 @@ class MatrixRoomMembersTest {
fun `getDirectRoomMember emit null if the room has only 1 member`() = runTest { fun `getDirectRoomMember emit null if the room has only 1 member`() = runTest {
val matrixRoom = FakeMatrixRoom( val matrixRoom = FakeMatrixRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
isEncrypted = true, initialRoomInfo = aRoomInfo(isDirect = true)
isDirect = true,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember( matrixRoom.getDirectRoomMember(
@ -95,9 +98,9 @@ class MatrixRoomMembersTest {
fun `getDirectRoomMember emit null if the room has only 3 members`() = runTest { fun `getDirectRoomMember emit null if the room has only 3 members`() = runTest {
val matrixRoom = FakeMatrixRoom( val matrixRoom = FakeMatrixRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
isEncrypted = true, ).apply {
isDirect = true, givenRoomInfo(aRoomInfo(isDirect = true))
) }
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember( matrixRoom.getDirectRoomMember(
MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2, roomMember3)) MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2, roomMember3))
@ -111,8 +114,7 @@ class MatrixRoomMembersTest {
fun `getDirectRoomMember emit null if the other member is not active`() = runTest { fun `getDirectRoomMember emit null if the other member is not active`() = runTest {
val matrixRoom = FakeMatrixRoom( val matrixRoom = FakeMatrixRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
isEncrypted = true, initialRoomInfo = aRoomInfo(isDirect = true),
isDirect = true,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember( matrixRoom.getDirectRoomMember(
@ -132,8 +134,10 @@ class MatrixRoomMembersTest {
fun `getDirectRoomMember emit the other member if there are 2 active members`() = runTest { fun `getDirectRoomMember emit the other member if there are 2 active members`() = runTest {
val matrixRoom = FakeMatrixRoom( val matrixRoom = FakeMatrixRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
isEncrypted = true, initialRoomInfo = aRoomInfo(
isDirect = true, isDirect = true,
activeMembersCount = 2,
)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
matrixRoom.getDirectRoomMember( matrixRoom.getDirectRoomMember(

View file

@ -64,7 +64,7 @@ class MediaGalleryPresenter @AssistedInject constructor(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var mode by remember { mutableStateOf(MediaGalleryMode.Images) } var mode by remember { mutableStateOf(MediaGalleryMode.Images) }
val roomInfo by room.roomInfoFlow.collectAsState(null) val roomInfo by room.roomInfoFlow.collectAsState()
var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) } var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) }
@ -139,7 +139,7 @@ class MediaGalleryPresenter @AssistedInject constructor(
} }
return MediaGalleryState( return MediaGalleryState(
roomName = roomInfo?.name ?: room.displayName, roomName = roomInfo.name.orEmpty(),
mode = mode, mode = mode,
groupedMediaItems = groupedMediaItems, groupedMediaItems = groupedMediaItems,
mediaBottomSheetState = mediaBottomSheetState, mediaBottomSheetState = mediaBottomSheetState,

View file

@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource
import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource
@ -51,7 +52,7 @@ class MediaGalleryPresenterTest {
startLambda = startLambda, startLambda = startLambda,
), ),
room = FakeMatrixRoom( room = FakeMatrixRoom(
displayName = A_ROOM_NAME, initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
createTimelineResult = { Result.success(FakeTimeline()) }, createTimelineResult = { Result.success(FakeTimeline()) },
) )
) )
@ -70,7 +71,7 @@ class MediaGalleryPresenterTest {
fun `present - change mode`() = runTest { fun `present - change mode`() = runTest {
val presenter = createMediaGalleryPresenter( val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom( room = FakeMatrixRoom(
displayName = A_ROOM_NAME, initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
createTimelineResult = { Result.success(FakeTimeline()) }, createTimelineResult = { Result.success(FakeTimeline()) },
) )
) )
@ -100,7 +101,7 @@ class MediaGalleryPresenterTest {
val presenter = createMediaGalleryPresenter( val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom( room = FakeMatrixRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
displayName = A_ROOM_NAME, initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
createTimelineResult = { Result.success(FakeTimeline()) }, createTimelineResult = { Result.success(FakeTimeline()) },
canRedactOwnResult = { Result.success(canDeleteOwn) } canRedactOwnResult = { Result.success(canDeleteOwn) }
) )
@ -143,7 +144,7 @@ class MediaGalleryPresenterTest {
val presenter = createMediaGalleryPresenter( val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom( room = FakeMatrixRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
displayName = A_ROOM_NAME, initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
createTimelineResult = { Result.success(FakeTimeline()) }, createTimelineResult = { Result.success(FakeTimeline()) },
canRedactOtherResult = { Result.success(canDeleteOther) } canRedactOtherResult = { Result.success(canDeleteOther) }
) )
@ -176,7 +177,7 @@ class MediaGalleryPresenterTest {
fun `present - delete bottom sheet`() = runTest { fun `present - delete bottom sheet`() = runTest {
val presenter = createMediaGalleryPresenter( val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom( room = FakeMatrixRoom(
displayName = A_ROOM_NAME, initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
createTimelineResult = { Result.success(FakeTimeline()) }, createTimelineResult = { Result.success(FakeTimeline()) },
) )
) )

View file

@ -152,8 +152,8 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
imageUriString = null, imageUriString = null,
imageMimeType = null, imageMimeType = null,
threadId = threadId, threadId = threadId,
roomName = room.displayName, roomName = room.info().name,
roomIsDm = room.isDm, roomIsDm = room.isDm(),
outGoingMessage = true, outGoingMessage = true,
) )
onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent) onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent)

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
@ -337,7 +338,14 @@ class NotificationBroadcastReceiverHandlerTest {
val matrixRoom = FakeMatrixRoom( val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline, liveTimeline = liveTimeline,
getUpdatedMemberResult = { Result.success(aRoomMember()) }, getUpdatedMemberResult = { Result.success(aRoomMember()) },
) ).apply {
givenRoomInfo(
aRoomInfo(
isDirect = true,
activeMembersCount = 2,
)
)
}
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> } val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> }
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult) val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult)
val sut = createNotificationBroadcastReceiverHandler( val sut = createNotificationBroadcastReceiverHandler(
@ -396,7 +404,14 @@ class NotificationBroadcastReceiverHandlerTest {
val matrixRoom = FakeMatrixRoom( val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline, liveTimeline = liveTimeline,
getUpdatedMemberResult = { Result.success(aRoomMember()) }, getUpdatedMemberResult = { Result.success(aRoomMember()) },
) ).apply {
givenRoomInfo(
aRoomInfo(
isDirect = true,
activeMembersCount = 2,
)
)
}
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> } val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> }
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult) val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult)
val sut = createNotificationBroadcastReceiverHandler( val sut = createNotificationBroadcastReceiverHandler(