diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt index 4f4e360f4fb..a4bfe0c60d7 100644 --- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt @@ -15,7 +15,7 @@ import io.element.android.x.di.AppComponent import io.element.android.x.di.DaggerAppComponent import io.element.android.x.info.logApplicationInfo 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 { override val daggerComponent: AppComponent = DaggerAppComponent.factory().create(this) @@ -24,7 +24,7 @@ class ElementXApplication : Application(), DaggerComponentOwner { super.onCreate() AppInitializer.getInstance(this).apply { initializeComponent(CrashInitializer::class.java) - initializeComponent(TracingInitializer::class.java) + initializeComponent(PlatformInitializer::class.java) initializeComponent(CacheCleanerInitializer::class.java) } logApplicationInfo(this) diff --git a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt index 4a7e12863e6..8525f6356b2 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt @@ -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.di.AppScope 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.preferences.api.store.AppPreferencesStore import io.element.android.services.analytics.api.AnalyticsService @@ -27,6 +28,8 @@ interface AppBindings { fun tracingService(): TracingService + fun platformService(): InitPlatformService + fun bugReporter(): BugReporter fun lockScreenService(): LockScreenService diff --git a/app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt similarity index 93% rename from app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt rename to app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt index ac225db2b0c..650480a0eee 100644 --- a/app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt +++ b/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt @@ -22,10 +22,11 @@ import timber.log.Timber private const val ELEMENT_X_TARGET = "elementx" -class TracingInitializer : Initializer { +class PlatformInitializer : Initializer { override fun create(context: Context) { val appBindings = context.bindings() val tracingService = appBindings.tracingService() + val platformService = appBindings.platformService() val bugReporter = appBindings.bugReporter() Timber.plant(tracingService.createTimberTree(ELEMENT_X_TARGET)) val preferencesStore = appBindings.preferencesStore() @@ -38,7 +39,7 @@ class TracingInitializer : Initializer { extraTargets = listOf(ELEMENT_X_TARGET), ) bugReporter.setCurrentTracingLogLevel(logLevel.name) - tracingService.setupTracing(tracingConfiguration) + platformService.init(tracingConfiguration) // Also set env variable for rust back trace Os.setenv("RUST_BACKTRACE", "1", true) } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt index 097e8a65317..8d34cfcfd44 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt @@ -38,7 +38,8 @@ class DefaultCallWidgetProvider @Inject constructor( val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() ?: elementCallBaseUrlProvider.provides(matrixClient) ?: 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( widgetSettings = widgetSettings, clientId = clientId, diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt index a567b2d3d75..4943e751850 100644 --- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt @@ -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.core.RoomId import io.element.android.libraries.matrix.api.room.isDm +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -74,10 +75,11 @@ private suspend fun showLeaveRoomAlert( confirmation: MutableState, ) { matrixClient.getRoom(roomId)?.use { room -> + val roomInfo = room.roomInfoFlow.first() confirmation.value = when { - room.isDm -> Dm(roomId) - !room.isPublic -> PrivateRoom(roomId) - room.joinedMemberCount == 1L -> LastUserInRoom(roomId) + roomInfo.isDm -> Dm(roomId) + !roomInfo.isPublic -> PrivateRoom(roomId) + roomInfo.joinedMembersCount == 1L -> LastUserInRoom(roomId) else -> Generic(roomId) } } diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterTest.kt index e9d229ad934..22407c93d42 100644 --- a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterTest.kt +++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterTest.kt @@ -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.FakeMatrixClient 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.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -52,7 +53,9 @@ class LeaveRoomPresenterTest { client = FakeMatrixClient().apply { givenGetRoomResult( 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 { givenGetRoomResult( roomId = A_ROOM_ID, - result = FakeMatrixRoom(isPublic = false), + result = FakeMatrixRoom().apply { + givenRoomInfo(aRoomInfo(isPublic = false)) + }, ) } ) @@ -92,7 +97,9 @@ class LeaveRoomPresenterTest { client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, - result = FakeMatrixRoom(joinedMemberCount = 1), + result = FakeMatrixRoom().apply { + givenRoomInfo(aRoomInfo(joinedMembersCount = 1)) + }, ) } ) @@ -112,7 +119,9 @@ class LeaveRoomPresenterTest { client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, - result = FakeMatrixRoom(activeMemberCount = 2, isDirect = true), + result = FakeMatrixRoom().apply { + givenRoomInfo(aRoomInfo(isDirect = true, activeMembersCount = 2)) + }, ) } ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index 359e556e696..eb5a8fee671 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -93,7 +93,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!! if (matrixHomeServerDetails.supportsOidcLogin) { // 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()) } else if (params.isAccountCreation) { val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index a8076d023c7..4ea6a1c9d3c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -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.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch @ContributesNode(RoomScope::class) class MessagesNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val coroutineScope: CoroutineScope, private val room: MatrixRoom, private val analyticsService: AnalyticsService, messageComposerPresenterFactory: MessageComposerPresenter.Factory, @@ -108,7 +111,7 @@ class MessagesNode @AssistedInject constructor( super.onBuilt() lifecycle.subscribe( onCreate = { - analyticsService.capture(room.toAnalyticsViewRoom()) + coroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) } }, onDestroy = { mediaPlayer.close() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 8a7c15a3ec5..0ace1a9f47d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -128,7 +128,7 @@ class MessagesPresenter @AssistedInject constructor( override fun present(): MessagesState { htmlConverterProvider.Update(currentUserId = room.sessionId) - val roomInfo by room.roomInfoFlow.collectAsState(null) + val roomInfo by room.roomInfoFlow.collectAsState() val localCoroutineScope = rememberCoroutineScope() val composerState = composerPresenter.present() val voiceMessageComposerState = voiceMessageComposerPresenter.present() @@ -147,13 +147,13 @@ class MessagesPresenter @AssistedInject constructor( val userEventPermissions by userEventPermissions(syncUpdateFlow.value) val roomName: AsyncData by remember { - derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized } + derivedStateOf { roomInfo.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized } } val roomAvatar: AsyncData by remember { - derivedStateOf { roomInfo?.avatarData()?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized } + derivedStateOf { AsyncData.Success(roomInfo.avatarData()) } } val heroes by remember { - derivedStateOf { roomInfo?.heroes().orEmpty().toPersistentList() } + derivedStateOf { roomInfo.heroes().toPersistentList() } } var hasDismissedInviteDialog by rememberSaveable { @@ -164,14 +164,20 @@ class MessagesPresenter @AssistedInject constructor( // as those will be handled by the timeline. withContext(dispatchers.io) { 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.Uninitialized) } 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) { - showReinvitePrompt = !hasDismissedInviteDialog && composerState.textEditorState.hasFocus() && room.isDm && room.activeMemberCount == 1L + showReinvitePrompt = !hasDismissedInviteDialog && composerHasFocus && roomInfo.isDm && roomInfo.activeMembersCount == 1L } } val isOnline by syncService.isOnline().collectAsState() @@ -189,9 +195,8 @@ class MessagesPresenter @AssistedInject constructor( val dmRoomMember by room.getDirectRoomMember(membersState) val roomMemberIdentityStateChanges = identityChangeState.roomMemberIdentityStateChanges - // TODO use `RoomInfo.isEncrypted` as a key here once it's available - LifecycleResumeEffect(dmRoomMember, roomMemberIdentityStateChanges) { - if (room.isEncrypted) { + LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) { + if (roomInfo.isEncrypted == true) { val dmRoomMemberId = dmRoomMember?.userId localCoroutineScope.launch { dmRoomMemberId?.let { userId -> @@ -275,7 +280,7 @@ class MessagesPresenter @AssistedInject constructor( return AvatarData( id = id.value, name = name, - url = avatarUrl ?: room.avatarUrl, + url = avatarUrl ?: room.info().avatarUrl, size = AvatarSize.TimelineRoom ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index f3a44559bc1..fce8e732e4d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -202,7 +202,7 @@ class MessageComposerPresenter @AssistedInject constructor( suspend fun canSendRoomMention(): Boolean { val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false) - return !room.isDm && userCanSendAtRoom + return !room.isDm() && userCanSendAtRoom } // This will trigger a search immediately when `@` is typed diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index d00e9e7d1af..9eed64ca2d5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -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.SnackbarMessage 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.canRedactOther 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.ui.room.isDmAsState import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction @@ -85,10 +85,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor( @Composable override fun present(): PinnedMessagesListState { - val timelineRoomInfo = remember { + val isDm by room.isDmAsState() + + val timelineRoomInfo = remember(isDm) { TimelineRoomInfo( - isDm = room.isDm, - name = room.displayName, + isDm = isDm, + name = room.info().name, // We don't need to compute those values userHasPermissionToSendMessage = false, userHasPermissionToSendReaction = false, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 8a496859d0f..4061be78c14 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -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.roomcall.api.RoomCallState 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.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId @@ -95,7 +96,7 @@ class TimelinePresenter @AssistedInject constructor( val lastReadReceiptId = rememberSaveable { mutableStateOf(null) } - val roomInfo by room.roomInfoFlow.collectAsState(initial = null) + val roomInfo by room.roomInfoFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() @@ -231,15 +232,15 @@ class TimelinePresenter @AssistedInject constructor( val typingNotificationState = typingNotificationPresenter.present() val roomCallState = roomCallStatePresenter.present() - val timelineRoomInfo by remember(typingNotificationState, roomCallState) { + val timelineRoomInfo by remember(typingNotificationState, roomCallState, roomInfo) { derivedStateOf { TimelineRoomInfo( - name = room.displayName, - isDm = room.isDm, + name = roomInfo.name, + isDm = roomInfo.isDm.orFalse(), userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendReaction = userHasPermissionToSendReaction, roomCallState = roomCallState, - pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(), + pinnedEventIds = roomInfo.pinnedEventIds.orEmpty(), typingNotificationState = typingNotificationState, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index a0ddc6bbc93..1f7d83401e5 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -536,15 +536,15 @@ class MessagesPresenterTest { fun `present - shows prompt to reinvite users in DM`() = runTest { val room = FakeMatrixRoom( sessionId = A_SESSION_ID, - isDirect = true, - activeMemberCount = 1L, canUserSendMessageResult = { _, _ -> Result.success(true) }, canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ).apply { + givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 1, activeMembersCount = 1)) + } val presenter = createMessagesPresenter(matrixRoom = room) presenter.testWithLifecycleOwner { val initialState = awaitItem() @@ -552,7 +552,8 @@ class MessagesPresenterTest { assertThat(initialState.showReinvitePrompt).isFalse() // When the input field is focused we show the alert (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true - skipItems(1) + // Skip intermediate states + skipItems(2) val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isTrue() // 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 { val room = FakeMatrixRoom( sessionId = A_SESSION_ID, - isDirect = false, - activeMemberCount = 1L, canUserSendMessageResult = { _, _ -> Result.success(true) }, canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ).apply { + givenRoomInfo(aRoomInfo(isDirect = false, joinedMembersCount = 1, activeMembersCount = 1)) + } val presenter = createMessagesPresenter(matrixRoom = room) presenter.testWithLifecycleOwner { val initialState = awaitItem() assertThat(initialState.showReinvitePrompt).isFalse() (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true + // Skip intermediate events + skipItems(1) val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isFalse() } @@ -590,20 +593,22 @@ class MessagesPresenterTest { fun `present - doesn't show reinvite prompt if other party is present`() = runTest { val room = FakeMatrixRoom( sessionId = A_SESSION_ID, - isDirect = true, - activeMemberCount = 2L, canUserSendMessageResult = { _, _ -> Result.success(true) }, canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ).apply { + givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 2, activeMembersCount = 2)) + } val presenter = createMessagesPresenter(matrixRoom = room) presenter.testWithLifecycleOwner { val initialState = awaitItem() assertThat(initialState.showReinvitePrompt).isFalse() (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true + // Skip intermediate events + skipItems(1) val focusedState = awaitItem() 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 { val room = FakeMatrixRoom( sessionId = A_SESSION_ID, - isEncrypted = true, - isDirect = true, - activeMemberCount = 2L, canUserSendMessageResult = { _, _ -> Result.success(true) }, canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, + initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true) ).apply { 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 presenter = createMessagesPresenter(matrixRoom = room, encryptionService = encryptionService) val lifecycleOwner = FakeLifecycleOwner() presenter.testWithLifecycleOwner(lifecycleOwner) { - skipItems(1) - val initialState = awaitItem() assertThat(initialState.dmUserVerificationState).isNull() + + skipItems(1) ensureAllEventsConsumed() lifecycleOwner.givenState(Lifecycle.State.RESUMED) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt index 29f64b831f6..2a82db690e3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt @@ -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.encryption.FakeEncryptionService 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.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -44,7 +45,9 @@ class IdentityChangeStatePresenterTest { @Test 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) presenter.test { val initialState = awaitItem() @@ -67,10 +70,7 @@ class IdentityChangeStatePresenterTest { @Test fun `present - when the clear room emits identity change, the presenter does not emit new state`() = runTest { - val room = FakeMatrixRoom( - isEncrypted = false, - enableEncryptionResult = { Result.success(Unit) } - ) + val room = FakeMatrixRoom(enableEncryptionResult = { Result.success(Unit) }) val presenter = createIdentityChangeStatePresenter(room) presenter.test { val initialState = awaitItem() @@ -85,8 +85,9 @@ class IdentityChangeStatePresenterTest { ) // No item emitted. expectNoEvents() - // Room become encrypted. - room.enableEncryption() + // Room becomes encrypted. + room.givenRoomInfo(aRoomInfo(isEncrypted = true)) + val finalItem = awaitItem() assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1) val value = finalItem.roomMemberIdentityStateChanges.first() @@ -99,7 +100,7 @@ class IdentityChangeStatePresenterTest { @Test fun `present - when the room emits identity change, the presenter emits new state with member details`() = runTest { - val room = FakeMatrixRoom(isEncrypted = true).apply { + val room = FakeMatrixRoom().apply { givenRoomMembersState( MatrixRoomMembersState.Ready( listOf( @@ -110,6 +111,7 @@ class IdentityChangeStatePresenterTest { ).toImmutableList() ) ) + givenRoomInfo(aRoomInfo(isEncrypted = true)) } val presenter = createIdentityChangeStatePresenter(room) presenter.test { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index b7ba51192bd..07205590c81 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -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.FakePermalinkParser 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.timeline.FakeTimeline 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) var canUserTriggerRoomNotificationResult = true val room = FakeMatrixRoom( - isDirect = false, canUserTriggerRoomNotificationResult = { Result.success(canUserTriggerRoomNotificationResult) }, typingNoticeResult = { Result.success(Unit) } ).apply { @@ -1007,6 +1007,7 @@ class MessageComposerPresenterTest { persistentListOf(currentUser, invitedUser, bob, david), ) ) + givenRoomInfo(aRoomInfo(isDirect = false)) } val flagsService = FakeFeatureFlagService( mapOf( @@ -1060,9 +1061,6 @@ class MessageComposerPresenterTest { 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 room = FakeMatrixRoom( - isDirect = true, - activeMemberCount = 2, - isEncrypted = true, canUserTriggerRoomNotificationResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) } ).apply { @@ -1071,6 +1069,12 @@ class MessageComposerPresenterTest { persistentListOf(currentUser, invitedUser, bob, david), ) ) + givenRoomInfo( + aRoomInfo( + isDirect = true, + activeMembersCount = 2, + ) + ) } val flagsService = FakeFeatureFlagService( mapOf( diff --git a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt index 4c3ddddb174..d04d40b4ef2 100644 --- a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt +++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt @@ -26,12 +26,12 @@ class RoomCallStatePresenter @Inject constructor( ) : Presenter { @Composable override fun present(): RoomCallState { - val roomInfo by room.roomInfoFlow.collectAsState(null) + val roomInfo by room.roomInfoFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value) val isUserInTheCall by remember { derivedStateOf { - room.sessionId in roomInfo?.activeRoomCallParticipants.orEmpty() + room.sessionId in roomInfo.activeRoomCallParticipants } } val currentCall by currentCallService.currentCall.collectAsState() @@ -41,7 +41,7 @@ class RoomCallStatePresenter @Inject constructor( } } val callState = when { - roomInfo?.hasRoomCall == true -> RoomCallState.OnGoing( + roomInfo.hasRoomCall -> RoomCallState.OnGoing( canJoinCall = canJoinCall, isUserInTheCall = isUserInTheCall, isUserLocallyInTheCall = isUserLocallyInTheCall, diff --git a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt index 55559294567..23791eb4441 100644 --- a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt +++ b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt @@ -58,12 +58,10 @@ class RoomCallStatePresenterTest { fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest { val room = FakeMatrixRoom( canUserJoinCallResult = { Result.success(false) }, - ).apply { - givenRoomInfo(aRoomInfo(hasRoomCall = true)) - } + initialRoomInfo = aRoomInfo(hasRoomCall = true), + ) val presenter = createRoomCallStatePresenter(matrixRoom = room) presenter.test { - skipItems(1) assertThat(awaitItem()).isEqualTo( RoomCallState.OnGoing( canJoinCall = false, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 631158f3bba..df1daf63867 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -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.securityandprivacy.permissions.securityAndPrivacyPermissionsAsState 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.featureflag.api.FeatureFlagService 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.RoomMember 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.powerlevels.canInvite 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.getCurrentRoomMember 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.roomMemberIdentityStateChange import io.element.android.services.analytics.api.AnalyticsService @@ -72,22 +71,22 @@ class RoomDetailsPresenter @Inject constructor( val scope = rememberCoroutineScope() val leaveRoomState = leaveRoomPresenter.present() val canShowNotificationSettings = remember { mutableStateOf(false) } - val roomInfo by room.roomInfoFlow.collectAsState(initial = null) + val roomInfo by room.roomInfoFlow.collectAsState() val isUserAdmin = room.isOwnUserAdmin() 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 roomTopic by remember { derivedStateOf { roomInfo?.topic ?: room.topic } } - val isFavorite by remember { derivedStateOf { roomInfo?.isFavorite.orFalse() } } - val joinRule by remember { derivedStateOf { roomInfo?.joinRule } } + val roomName by remember { derivedStateOf { roomInfo.name?.trim().orEmpty() } } + val roomTopic by remember { derivedStateOf { roomInfo.topic } } + val isFavorite by remember { derivedStateOf { roomInfo.isFavorite } } + val joinRule by remember { derivedStateOf { roomInfo.joinRule } } val canShowPinnedMessages = isPinnedMessagesFeatureEnabled() var canShowMediaGallery by remember { mutableStateOf(false) } LaunchedEffect(Unit) { canShowMediaGallery = featureFlagService.isFeatureEnabled(FeatureFlags.MediaGallery) } - val pinnedMessagesCount by remember { derivedStateOf { roomInfo?.pinnedEventIds?.size } } + val pinnedMessagesCount by remember { derivedStateOf { roomInfo.pinnedEventIds.size } } LaunchedEffect(Unit) { canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) @@ -100,6 +99,9 @@ class RoomDetailsPresenter @Inject constructor( val membersState by room.membersStateFlow.collectAsState() 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 canEditAvatar by getCanSendState(membersState, StateEventType.ROOM_AVATAR) val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC) @@ -108,6 +110,7 @@ class RoomDetailsPresenter @Inject constructor( val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) val roomType = getRoomType(dmMember, currentMember) val roomCallState = roomCallStatePresenter.present() + val joinedMemberCount by remember { derivedStateOf { roomInfo.joinedMembersCount } } val topicState = remember(canEditTopic, roomTopic, roomType) { val topic = roomTopic @@ -140,7 +143,7 @@ class RoomDetailsPresenter @Inject constructor( } RoomDetailsEvent.UnmuteNotification -> { 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) @@ -165,11 +168,11 @@ class RoomDetailsPresenter @Inject constructor( return RoomDetailsState( roomId = room.roomId, roomName = roomName, - roomAlias = room.canonicalAlias, + roomAlias = canonicalAlias, roomAvatarUrl = roomAvatar, roomTopic = topicState, - memberCount = room.joinedMemberCount, - isEncrypted = room.isEncrypted, + memberCount = joinedMemberCount, + isEncrypted = isEncrypted, canInvite = canInvite, canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room, canShowNotificationSettings = canShowNotificationSettings.value, @@ -179,9 +182,9 @@ class RoomDetailsPresenter @Inject constructor( leaveRoomState = leaveRoomState, roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), isFavorite = isFavorite, - displayRolesAndPermissionsSettings = !room.isDm && isUserAdmin, + displayRolesAndPermissionsSettings = !isDm && isUserAdmin, isPublic = joinRule == JoinRule.Public, - heroes = roomInfo?.heroes.orEmpty().toPersistentList(), + heroes = roomInfo.heroes.toPersistentList(), canShowPinnedMessages = canShowPinnedMessages, canShowMediaGallery = canShowMediaGallery, pinnedMessagesCount = pinnedMessagesCount, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 89f82319c58..e154c7ea154 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -183,7 +183,7 @@ class RoomMemberListPresenter @AssistedInject constructor( } private suspend fun RoomMember.withIdentityState(identityStates: ImmutableMap): RoomMemberWithIdentityState { - return if (!room.isEncrypted) { + return if (room.info().isEncrypted != true) { RoomMemberWithIdentityState(this, null) } else { val identityState = identityStates[userId] ?: encryptionService.getUserIdentity(userId).getOrNull() diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index 4e293395bc5..9a5d456cbef 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -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.ui.room.getRoomMemberAsState 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.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -47,6 +50,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( private val userProfilePresenter = userProfilePresenterFactory.create(roomMemberId) + @OptIn(ExperimentalCoroutinesApi::class) @Composable override fun present(): UserProfileState { val coroutineScope = rememberCoroutineScope() @@ -75,21 +79,22 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( val userProfileState = userProfilePresenter.present() val identityStateChanges by produceState(initialValue = null) { - if (room.isEncrypted) { - // Fetch the initial identity state manually - val identityState = encryptionService.getUserIdentity(roomMemberId).getOrNull() - value = identityState?.let { IdentityStateChange(roomMemberId, it) } + room.roomInfoFlow.filter { it.isEncrypted == true } + .flatMapLatest { + // Fetch the initial identity state manually + val identityState = encryptionService.getUserIdentity(roomMemberId).getOrNull() + value = identityState?.let { IdentityStateChange(roomMemberId, it) } - // Subscribe to the identity changes - room.roomMemberIdentityStateChange() - .map { it.find { it.identityRoomMember.userId == roomMemberId } } - .map { roomMemberIdentityStateChange -> - // If we didn't receive any info, manually fetch it - roomMemberIdentityStateChange?.identityState ?: encryptionService.getUserIdentity(roomMemberId).getOrNull() - } - .filterNotNull() - .collect { value = IdentityStateChange(roomMemberId, it) } - } + // Subscribe to the identity changes + room.roomMemberIdentityStateChange() + .map { it.find { it.identityRoomMember.userId == roomMemberId } } + .map { roomMemberIdentityStateChange -> + // If we didn't receive any info, manually fetch it + roomMemberIdentityStateChange?.identityState ?: encryptionService.getUserIdentity(roomMemberId).getOrNull() + } + .filterNotNull() + } + .collect { value = IdentityStateChange(roomMemberId, it) } } val verificationState = remember(identityStateChanges) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt index cbd9d3aca08..e9d4a915143 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt @@ -50,7 +50,7 @@ class RoomMembersModerationPresenter @Inject constructor( val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canBan by room.canBanAsState(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 canDisplayModerationActions by remember { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt index 77bdafe94a8..7dae51a1a21 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable @@ -78,11 +79,23 @@ class RoomNotificationSettingsPresenter @AssistedInject constructor( 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) { getDefaultRoomNotificationMode(defaultRoomNotificationMode) fetchNotificationSettings(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) { @@ -113,7 +126,7 @@ class RoomNotificationSettingsPresenter @AssistedInject constructor( return RoomNotificationSettingsState( showUserDefinedSettingStyle = showUserDefinedSettingStyle, - roomName = room.displayName, + roomName = displayName.orEmpty(), roomNotificationSettings = roomNotificationSettings.value, pendingRoomNotificationMode = pendingRoomNotificationMode.value, pendingSetDefault = pendingSetDefault.value, @@ -143,16 +156,18 @@ class RoomNotificationSettingsPresenter @AssistedInject constructor( roomNotificationSettings: MutableState> ) = launch { suspend { + val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() pendingModeState.value = null - notificationSettingsService.getRoomNotificationSettings(room.roomId, room.isEncrypted, room.isOneToOne).getOrThrow() + notificationSettingsService.getRoomNotificationSettings(room.roomId, isEncrypted, room.isOneToOne).getOrThrow() }.runCatchingUpdatingState(roomNotificationSettings) } private fun CoroutineScope.getDefaultRoomNotificationMode( defaultRoomNotificationMode: MutableState ) = launch { + val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode( - room.isEncrypted, + isEncrypted, room.isOneToOne ).getOrThrow() } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt index 5cd4bd17376..37e72d158f0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt @@ -39,7 +39,7 @@ class RolesAndPermissionsPresenter @Inject constructor( @Composable override fun present(): RolesAndPermissionsState { val coroutineScope = rememberCoroutineScope() - val roomInfo by room.roomInfoFlow.collectAsState(initial = null) + val roomInfo by room.roomInfoFlow.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 // level state Event. @@ -109,8 +109,8 @@ class RolesAndPermissionsPresenter @Inject constructor( } } - private fun MatrixRoomInfo?.userCountWithRole(userIds: List, role: RoomMember.Role): Int { - return this?.userPowerLevels.orEmpty().count { (userId, level) -> + private fun MatrixRoomInfo.userCountWithRole(userIds: List, role: RoomMember.Role): Int { + return this.userPowerLevels.count { (userId, level) -> RoomMember.Role.forPowerLevel(level) == role && userId in userIds } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt index 392f9da0961..8878dc8876a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt @@ -106,10 +106,10 @@ class ChangeRolesPresenter @AssistedInject constructor( 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 { // 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 } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index 769ff87baee..d6f27a9a5bb 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -57,7 +57,7 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val homeserverName = remember { matrixClient.userIdServerName() } val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val roomInfo = room.roomInfoFlow.collectAsState(null) + val roomInfo by room.roomInfoFlow.collectAsState() val savedIsVisibleInRoomDirectory = remember { mutableStateOf>(AsyncData.Uninitialized) } LaunchedEffect(Unit) { @@ -66,12 +66,13 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( val savedSettings by remember { derivedStateOf { + val historyVisibility = roomInfo.historyVisibility.map() SecurityAndPrivacySettings( - roomAccess = roomInfo.value?.joinRule.map(), - isEncrypted = room.isEncrypted, + roomAccess = roomInfo.joinRule.map(), + isEncrypted = roomInfo.isEncrypted == true, isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory.value, - historyVisibility = roomInfo.value?.historyVisibility.map(), - address = roomInfo.value?.firstDisplayableAlias(homeserverName)?.value, + historyVisibility = historyVisibility, + address = roomInfo.firstDisplayableAlias(homeserverName)?.value, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt index c494443f1bf..2c581e45842 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt @@ -9,6 +9,8 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroom import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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.core.RoomAlias 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.roomAliasFromName import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity @@ -45,15 +48,16 @@ class EditRoomAddressPresenter @AssistedInject constructor( @Composable override fun present(): EditRoomAddressState { val coroutineScope = rememberCoroutineScope() + val roomInfo by room.roomInfoFlow.collectAsState() val homeserverName = remember { client.userIdServerName() } val roomAddressValidity = remember { mutableStateOf(RoomAddressValidity.Unknown) } - val savedRoomAddress = remember { room.firstAliasMatching(homeserverName)?.addressName() } + val savedRoomAddress by remember { derivedStateOf { roomInfo.firstAliasMatching(homeserverName)?.addressName() } } val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } var newRoomAddress by remember { mutableStateOf( - savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(room.displayName) + savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(roomInfo.name.orEmpty()) ) } @@ -97,8 +101,9 @@ class EditRoomAddressPresenter @AssistedInject constructor( newRoomAddress: String, ) = launch { suspend { - val savedCanonicalAlias = room.canonicalAlias - val savedAliasFromHomeserver = room.firstAliasMatching(serverName) + val roomInfo = room.info() + val savedCanonicalAlias = roomInfo.canonicalAlias + val savedAliasFromHomeserver = roomInfo.firstAliasMatching(serverName) val newRoomAlias = client.roomAliasFromName(newRoomAddress) ?: throw IllegalArgumentException("Invalid room address") // First publish the new alias in the room directory @@ -112,7 +117,7 @@ class EditRoomAddressPresenter @AssistedInject constructor( when { // 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) -> { - val newAlternativeAliases = room.alternativeAliases.filter { it != savedAliasFromHomeserver } + val newAlternativeAliases = roomInfo.alternativeAliases.filter { it != savedAliasFromHomeserver } room.updateCanonicalAlias(newRoomAlias, newAlternativeAliases).getOrThrow() } // 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 add(newRoomAlias) // 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() } @@ -134,7 +139,7 @@ class EditRoomAddressPresenter @AssistedInject constructor( /** * 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 if (canonicalAlias?.matchesServer(serverName) == true) { return canonicalAlias diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt index d9cc77be76c..3cf5c02e47f 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt @@ -7,34 +7,43 @@ 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.SessionId 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.StateEventType 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.A_ROOM_ALIAS 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_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.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.tests.testutils.lambda.lambdaError fun aMatrixRoom( + sessionId: SessionId = A_SESSION_ID, roomId: RoomId = A_ROOM_ID, displayName: String = A_ROOM_NAME, rawName: String? = displayName, topic: String? = A_ROOM_TOPIC, avatarUrl: String? = AN_AVATAR_URL, + canonicalAlias: RoomAlias? = A_ROOM_ALIAS, isEncrypted: Boolean = true, isPublic: Boolean = true, isDirect: Boolean = false, joinRule: JoinRule? = null, + activeMemberCount: Long = 1, + joinedMemberCount: Long = 1, + invitedMemberCount: Long = 0, notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), - emitRoomInfo: Boolean = false, canInviteResult: (UserId) -> Result = { lambdaError() }, canBanResult: (UserId) -> Result = { lambdaError() }, + canKickResult: (UserId) -> Result = { lambdaError() }, canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() }, userDisplayNameResult: (UserId) -> Result = { lambdaError() }, userAvatarUrlResult: () -> Result = { lambdaError() }, @@ -44,17 +53,20 @@ fun aMatrixRoom( removeAvatarResult: () -> Result = { lambdaError() }, canUserJoinCallResult: (UserId) -> Result = { lambdaError() }, getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, + userRoleResult: () -> Result = { lambdaError() }, + kickUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, + banUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, + unBanUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, + updateCanonicalAliasResult: (RoomAlias?, List) -> Result = { _, _ -> lambdaError() }, + publishRoomAliasInRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, + removeRoomAliasFromRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, ) = FakeMatrixRoom( + sessionId = sessionId, roomId = roomId, - displayName = displayName, - topic = topic, - avatarUrl = avatarUrl, - isEncrypted = isEncrypted, - isPublic = isPublic, - isDirect = isDirect, notificationSettingsService = notificationSettingsService, canInviteResult = canInviteResult, canBanResult = canBanResult, + canKickResult = canKickResult, canSendStateResult = canSendStateResult, userDisplayNameResult = userDisplayNameResult, userAvatarUrlResult = userAvatarUrlResult, @@ -64,17 +76,25 @@ fun aMatrixRoom( removeAvatarResult = removeAvatarResult, canUserJoinCallResult = canUserJoinCallResult, getUpdatedMemberResult = getUpdatedMemberResult, -).apply { - if (emitRoomInfo) { - givenRoomInfo( - aRoomInfo( - name = displayName, - rawName = rawName, - topic = topic, - avatarUrl = avatarUrl, - isDirect = isDirect, - joinRule = joinRule, - ) - ) - } -} + userRoleResult = userRoleResult, + kickUserResult = kickUserResult, + banUserResult = banUserResult, + unBanUserResult = unBanUserResult, + updateCanonicalAliasResult = updateCanonicalAliasResult, + publishRoomAliasInRoomDirectoryResult = publishRoomAliasInRoomDirectoryResult, + removeRoomAliasFromRoomDirectoryResult = removeRoomAliasFromRoomDirectoryResult, + initialRoomInfo = aRoomInfo( + name = displayName, + rawName = rawName, + topic = topic, + avatarUrl = avatarUrl, + canonicalAlias = canonicalAlias, + isDirect = isDirect, + isPublic = isPublic, + isEncrypted = isEncrypted, + joinRule = joinRule, + joinedMembersCount = joinedMemberCount, + activeMembersCount = activeMemberCount, + invitedMembersCount = invitedMemberCount, + ) +) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt index 610843b7c61..07d10ede633 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt @@ -110,7 +110,7 @@ class RoomDetailsPresenterTest { } @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( canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, @@ -118,22 +118,22 @@ class RoomDetailsPresenterTest { ) val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { - skipItems(1) val initialState = awaitItem() assertThat(initialState.roomId).isEqualTo(room.roomId) - assertThat(initialState.roomName).isEqualTo(room.displayName) - assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl) - assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!)) - assertThat(initialState.memberCount).isEqualTo(room.joinedMemberCount) - assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted) + assertThat(initialState.roomName).isEqualTo(room.info().name) + assertThat(initialState.roomAvatarUrl).isEqualTo(room.info().avatarUrl) + assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.info().topic!!)) + assertThat(initialState.memberCount).isEqualTo(room.info().joinedMembersCount) assertThat(initialState.canShowPinnedMessages).isTrue() - assertThat(initialState.pinnedMessagesCount).isNull() + assertThat(initialState.pinnedMessagesCount).isEqualTo(0) assertThat(initialState.canShowSecurityAndPrivacy).isFalse() + + cancelAndIgnoreRemainingEvents() } } @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( name = A_ROOM_NAME, topic = A_ROOM_TOPIC, @@ -170,7 +170,7 @@ class RoomDetailsPresenterTest { val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { val initialState = awaitItem() - assertThat(initialState.roomName).isEqualTo(room.displayName) + assertThat(initialState.roomName).isEqualTo(room.info().name) cancelAndIgnoreRemainingEvents() } @@ -181,8 +181,6 @@ class RoomDetailsPresenterTest { val myRoomMember = aRoomMember(A_SESSION_ID) val otherRoomMember = aRoomMember(A_USER_ID_2) val room = aMatrixRoom( - isEncrypted = true, - isDirect = true, canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, @@ -196,6 +194,13 @@ class RoomDetailsPresenterTest { ).apply { val roomMembers = persistentListOf(myRoomMember, otherRoomMember) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) + + givenRoomInfo( + aRoomInfo( + isEncrypted = true, + isDirect = true, + ) + ) } val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { @@ -287,8 +292,6 @@ class RoomDetailsPresenterTest { val myRoomMember = aRoomMember(A_SESSION_ID) val otherRoomMember = aRoomMember(A_USER_ID_2) val room = aMatrixRoom( - isEncrypted = true, - isDirect = true, canSendStateResult = { _, stateEventType -> when (stateEventType) { StateEventType.ROOM_TOPIC, @@ -309,6 +312,13 @@ class RoomDetailsPresenterTest { ).apply { val roomMembers = persistentListOf(myRoomMember, otherRoomMember) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) + + givenRoomInfo( + aRoomInfo( + isEncrypted = true, + isDirect = true, + ) + ) } val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { @@ -318,7 +328,7 @@ class RoomDetailsPresenterTest { val settledState = awaitItem() assertThat(settledState.canEdit).isFalse() // 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() } @@ -329,7 +339,6 @@ class RoomDetailsPresenterTest { val myRoomMember = aRoomMember(A_SESSION_ID) val otherRoomMember = aRoomMember(A_USER_ID_2) val room = aMatrixRoom( - isEncrypted = true, isDirect = true, topic = null, canSendStateResult = { _, stateEventType -> @@ -352,6 +361,14 @@ class RoomDetailsPresenterTest { ).apply { val roomMembers = persistentListOf(myRoomMember, otherRoomMember) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) + + givenRoomInfo( + aRoomInfo( + isDirect = true, + activeMembersCount = 2, + topic = null, + ) + ) } val presenter = createRoomDetailsPresenter(room) @@ -630,7 +647,6 @@ class RoomDetailsPresenterTest { @Test fun `present - show knock requests`() = runTest { val room = aMatrixRoom( - emitRoomInfo = true, canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenterTest.kt index 26c9f99105d..763f94c3100 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenterTest.kt @@ -93,7 +93,6 @@ class RoomDetailsEditPresenterTest { avatarUrl = AN_AVATAR_URL, displayName = A_ROOM_NAME, rawName = A_ROOM_RAW_NAME, - emitRoomInfo = true, canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} @@ -106,7 +105,7 @@ class RoomDetailsEditPresenterTest { assertThat(initialState.roomId).isEqualTo(room.roomId) assertThat(initialState.roomRawName).isEqualTo(A_ROOM_RAW_NAME) assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri) - assertThat(initialState.roomTopic).isEqualTo(room.topic.orEmpty()) + assertThat(initialState.roomTopic).isEqualTo(room.info().topic.orEmpty()) assertThat(initialState.avatarActions).containsExactly( AvatarAction.ChoosePhoto, AvatarAction.TakePhoto, @@ -220,7 +219,6 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} @@ -266,7 +264,6 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(anotherAvatarUri) @@ -291,7 +288,6 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(anotherAvatarUri) @@ -319,8 +315,7 @@ class RoomDetailsEditPresenterTest { stateWithNewAvatar.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto)) val stateWithNewAvatar2 = awaitItem() assertThat(stateWithNewAvatar2.roomAvatarUrl).isEqualTo(roomAvatarUri) - deleteCallback.assertions().isCalledExactly(4).withSequence( - listOf(value(null)), + deleteCallback.assertions().isCalledExactly(3).withSequence( listOf(value(null)), listOf(value(roomAvatarUri)), listOf(value(anotherAvatarUri)), @@ -334,7 +329,6 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(roomAvatarUri) @@ -385,7 +379,6 @@ class RoomDetailsEditPresenterTest { topic = null, displayName = "fallback", avatarUrl = null, - emitRoomInfo = true, canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(roomAvatarUri) @@ -439,7 +432,6 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, setNameResult = setNameResult, setTopicResult = setTopicResult, removeAvatarResult = removeAvatarResult, @@ -553,7 +545,7 @@ class RoomDetailsEditPresenterTest { updateAvatarResult.assertions().isCalledOnce().with(value(MimeTypes.Jpeg), value(fakeFileContents)) deleteCallback.assertions().isCalledExactly(2).withSequence( listOf(value(null)), - listOf(value(null)), + listOf(value(roomAvatarUri)), ) } } @@ -588,11 +580,10 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, setNameResult = { Result.failure(Throwable("!")) }, canSendStateResult = { _, _ -> Result.success(true) } ) - saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomName("New name")) + saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomName("New name"), deleteCallbackNumberOfInvocation = 1) } @Test @@ -601,11 +592,10 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, setTopicResult = { Result.failure(Throwable("!")) }, canSendStateResult = { _, _ -> Result.success(true) } ) - saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomTopic("New topic")) + saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomTopic("New topic"), deleteCallbackNumberOfInvocation = 1) } @Test @@ -614,11 +604,10 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, removeAvatarResult = { Result.failure(Throwable("!")) }, canSendStateResult = { _, _ -> Result.success(true) } ) - saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove), deleteCallbackNumberOfInvocation = 3) + saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove), deleteCallbackNumberOfInvocation = 2) } @Test @@ -628,11 +617,10 @@ class RoomDetailsEditPresenterTest { topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, updateAvatarResult = { _, _ -> Result.failure(Throwable("!")) }, canSendStateResult = { _, _ -> Result.success(true) } ) - saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto), deleteCallbackNumberOfInvocation = 3) + saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto), deleteCallbackNumberOfInvocation = 2) } @Test @@ -704,6 +692,6 @@ class RoomDetailsEditPresenterTest { } private suspend fun ReceiveTurbine.awaitFirstItem(): T { - skipItems(2) + skipItems(1) return awaitItem() } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt index a2c82531062..1506f50f42c 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt @@ -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.encryption.FakeEncryptionService 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.consumeItemsUntilPredicate import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -167,10 +168,10 @@ class RoomMemberDetailsPresenterTest { @Test fun `present - when user's identity is verified, the value in the state is VERIFIED`() = runTest { val room = FakeMatrixRoom( - isEncrypted = true, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, userDisplayNameResult = { Result.success("A custom name") }, userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), ) val encryptionService = FakeEncryptionService( getUserIdentityResult = { Result.success(IdentityState.Verified) }, @@ -186,10 +187,10 @@ class RoomMemberDetailsPresenterTest { @Test fun `present - when user's identity is unknown, the value in the state is UNKNOWN`() = runTest { val room = FakeMatrixRoom( - isEncrypted = true, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, userDisplayNameResult = { Result.success("A custom name") }, userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), ) val encryptionService = FakeEncryptionService( getUserIdentityResult = { Result.success(null) }, @@ -205,10 +206,10 @@ class RoomMemberDetailsPresenterTest { @Test fun `present - when user's identity is pinned, the value in the state is UNVERIFIED`() = runTest { val room = FakeMatrixRoom( - isEncrypted = true, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, userDisplayNameResult = { Result.success("A custom name") }, userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), ) val encryptionService = FakeEncryptionService( getUserIdentityResult = { Result.success(IdentityState.Pinned) }, @@ -224,10 +225,10 @@ class RoomMemberDetailsPresenterTest { @Test fun `present - when user's identity is pin violation, the value in the state is UNVERIFIED`() = runTest { val room = FakeMatrixRoom( - isEncrypted = true, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, userDisplayNameResult = { Result.success("A custom name") }, userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), ) val encryptionService = FakeEncryptionService( getUserIdentityResult = { Result.success(IdentityState.PinViolation) }, @@ -243,10 +244,10 @@ class RoomMemberDetailsPresenterTest { @Test fun `present - when user's identity has a verification violation, the value in the state is VERIFICATION_VIOLATION`() = runTest { val room = FakeMatrixRoom( - isEncrypted = true, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, userDisplayNameResult = { Result.success("A custom name") }, userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), ) val encryptionService = FakeEncryptionService( getUserIdentityResult = { Result.success(IdentityState.VerificationViolation) }, @@ -262,10 +263,10 @@ class RoomMemberDetailsPresenterTest { @Test fun `present - user identity updates in real time if the room is encrypted`() = runTest { val room = FakeMatrixRoom( - isEncrypted = true, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, userDisplayNameResult = { Result.success("A custom name") }, userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), ) val encryptionService = FakeEncryptionService( getUserIdentityResult = { Result.success(null) }, @@ -291,10 +292,10 @@ class RoomMemberDetailsPresenterTest { @Test fun `present - user identity can't update in real time if the room is not encrypted`() = runTest { val room = FakeMatrixRoom( - isEncrypted = false, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, userDisplayNameResult = { Result.success("A custom name") }, userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = false), ) val encryptionService = FakeEncryptionService( getUserIdentityResult = { Result.success(null) }, @@ -315,10 +316,10 @@ class RoomMemberDetailsPresenterTest { @Test fun `present - handles WithdrawVerification action`() = runTest { val room = FakeMatrixRoom( - isEncrypted = true, getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, userDisplayNameResult = { Result.success("A custom name") }, userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), ) val withdrawVerificationResult = lambdaRecorder> { Result.success(Unit) } val encryptionService = FakeEncryptionService( diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenterTest.kt index d569a93baa5..d86fc499251 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenterTest.kt @@ -12,6 +12,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat 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.aVictor import io.element.android.libraries.architecture.AsyncAction @@ -34,8 +35,7 @@ import org.junit.Test class RoomMembersModerationPresenterTest { @Test fun `canDisplayModerationActions - when room is DM is false`() = runTest { - val room = FakeMatrixRoom( - isDirect = true, + val room = aMatrixRoom( isPublic = true, activeMemberCount = 2, canKickResult = { Result.success(true) }, @@ -52,8 +52,7 @@ class RoomMembersModerationPresenterTest { @Test fun `canDisplayModerationActions - when user can kick other users, FF is enabled and room is not a DM returns true`() = runTest { - val room = FakeMatrixRoom( - isDirect = false, + val room = aMatrixRoom( activeMemberCount = 10, canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, @@ -68,8 +67,7 @@ class RoomMembersModerationPresenterTest { @Test fun `canDisplayModerationActions - when user can ban other users, FF is enabled and room is not a DM returns true`() = runTest { - val room = FakeMatrixRoom( - isDirect = false, + val room = aMatrixRoom( activeMemberCount = 10, canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, @@ -84,7 +82,7 @@ class RoomMembersModerationPresenterTest { @Test fun `present - SelectRoomMember when the current user has permissions displays member actions`() = runTest { - val room = FakeMatrixRoom( + val room = aMatrixRoom( canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, @@ -110,7 +108,7 @@ class RoomMembersModerationPresenterTest { @Test 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, canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, @@ -136,7 +134,7 @@ class RoomMembersModerationPresenterTest { @Test 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 room = FakeMatrixRoom( + val room = aMatrixRoom( canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, @@ -157,7 +155,7 @@ class RoomMembersModerationPresenterTest { @Test fun `present - Kick removes the user`() = runTest { val analyticsService = FakeAnalyticsService() - val room = FakeMatrixRoom( + val room = aMatrixRoom( canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, @@ -186,7 +184,7 @@ class RoomMembersModerationPresenterTest { @Test fun `present - BanUser requires confirmation and then bans the user`() = runTest { val analyticsService = FakeAnalyticsService() - val room = FakeMatrixRoom( + val room = aMatrixRoom( canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, @@ -222,7 +220,7 @@ class RoomMembersModerationPresenterTest { fun `present - UnbanUser requires confirmation and then unbans the user`() = runTest { val analyticsService = FakeAnalyticsService() val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN) - val room = FakeMatrixRoom( + val room = aMatrixRoom( canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, @@ -254,7 +252,7 @@ class RoomMembersModerationPresenterTest { @Test fun `present - Reset removes the selected user and actions`() = runTest { - val room = FakeMatrixRoom( + val room = aMatrixRoom( canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, userRoleResult = { Result.success(RoomMember.Role.USER) }, @@ -276,7 +274,7 @@ class RoomMembersModerationPresenterTest { @Test fun `present - Reset resets any async actions`() = runTest { - val room = FakeMatrixRoom( + val room = aMatrixRoom( canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, kickUserResult = { _, _ -> Result.failure(Throwable("Eek")) }, @@ -324,7 +322,7 @@ class RoomMembersModerationPresenterTest { } private fun TestScope.createRoomMembersModerationPresenter( - matrixRoom: FakeMatrixRoom = FakeMatrixRoom(), + matrixRoom: FakeMatrixRoom = aMatrixRoom(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(), ): RoomMembersModerationPresenter { diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenterTest.kt index 1d94c720111..1c419c9d1f4 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenterTest.kt @@ -56,17 +56,15 @@ class SecurityAndPrivacyPresenterTest { fun `present - room info change updates saved and edited settings`() = runTest { val room = FakeMatrixRoom( canSendStateResult = { _, _ -> Result.success(true) }, + initialRoomInfo = aRoomInfo( + joinRule = JoinRule.Public, + historyVisibility = RoomHistoryVisibility.WorldReadable, + canonicalAlias = A_ROOM_ALIAS, + ) ) val presenter = createSecurityAndPrivacyPresenter(room = room) presenter.test { - skipItems(2) - room.givenRoomInfo( - aRoomInfo( - joinRule = JoinRule.Public, - historyVisibility = RoomHistoryVisibility.WorldReadable, - canonicalAlias = A_ROOM_ALIAS, - ) - ) + skipItems(1) with(awaitItem()) { assertThat(editedSettings).isEqualTo(savedSettings) assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) @@ -151,6 +149,7 @@ class SecurityAndPrivacyPresenterTest { assertThat(canBeSaved).isTrue() eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) } + skipItems(1) with(awaitItem()) { assertThat(editedSettings.isEncrypted).isFalse() assertThat(canBeSaved).isFalse() @@ -162,7 +161,8 @@ class SecurityAndPrivacyPresenterTest { fun `present - room visibility loading and change`() = runTest { val room = FakeMatrixRoom( canSendStateResult = { _, _ -> Result.success(true) }, - roomVisibilityResult = { Result.success(RoomVisibility.Private) } + roomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared) ) val presenter = createSecurityAndPrivacyPresenter(room = room) presenter.test { @@ -212,7 +212,8 @@ class SecurityAndPrivacyPresenterTest { updateJoinRuleResult = updateJoinRuleLambda, updateRoomVisibilityResult = updateRoomVisibilityLambda, 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) presenter.test { @@ -245,6 +246,7 @@ class SecurityAndPrivacyPresenterTest { aRoomInfo( joinRule = JoinRule.Public, historyVisibility = RoomHistoryVisibility.WorldReadable, + isEncrypted = true, ) ) // Saved settings are updated 3 times to match the edited settings @@ -275,7 +277,8 @@ class SecurityAndPrivacyPresenterTest { updateJoinRuleResult = updateJoinRuleLambda, updateRoomVisibilityResult = updateRoomVisibilityLambda, 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) presenter.test { @@ -311,7 +314,7 @@ class SecurityAndPrivacyPresenterTest { ) ) // Saved settings are updated 2 times to match the edited settings - skipItems(2) + skipItems(3) with(awaitItem()) { assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(savedSettings.isVisibleInRoomDirectory).isNotEqualTo(editedSettings.isVisibleInRoomDirectory) @@ -328,7 +331,8 @@ class SecurityAndPrivacyPresenterTest { serverName: String = "matrix.org", room: MatrixRoom = FakeMatrixRoom( 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(), ): SecurityAndPrivacyPresenter { diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenterTest.kt index 8c30f3d4fa4..845e2c02725 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenterTest.kt @@ -8,6 +8,7 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress 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.SecurityAndPrivacyNavigator import io.element.android.libraries.architecture.AsyncAction @@ -31,7 +32,9 @@ import java.util.Optional class EditRoomAddressPresenterTest { @Test fun `present - initial state no address`() = runTest { - val presenter = createEditRoomAddressPresenter() + val presenter = createEditRoomAddressPresenter( + room = aMatrixRoom(displayName = "") + ) presenter.test { with(awaitItem()) { assertThat(homeserverName).isEqualTo("matrix.org") @@ -45,7 +48,7 @@ class EditRoomAddressPresenterTest { @Test fun `present - initial state address matching own homeserver`() = runTest { - val room = FakeMatrixRoom( + val room = aMatrixRoom( canonicalAlias = RoomAlias("#canonical:matrix.org"), ) val presenter = createEditRoomAddressPresenter(room = room) @@ -62,7 +65,8 @@ class EditRoomAddressPresenterTest { @Test fun `present - initial state address not matching own homeserver`() = runTest { - val room = FakeMatrixRoom( + val room = aMatrixRoom( + displayName = "", canonicalAlias = RoomAlias("#canonical:notmatrix.org"), ) val presenter = createEditRoomAddressPresenter(room = room) @@ -190,7 +194,7 @@ class EditRoomAddressPresenterTest { val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda) val canonicalAlias = RoomAlias("#canonical:matrix.org") - val room = FakeMatrixRoom( + val room = aMatrixRoom( canonicalAlias = canonicalAlias, updateCanonicalAliasResult = updateCanonicalAliasResult, publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult, @@ -240,7 +244,7 @@ class EditRoomAddressPresenterTest { val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda) val canonicalAlias = RoomAlias("#canonical:notmatrix.org") - val room = FakeMatrixRoom( + val room = aMatrixRoom( canonicalAlias = canonicalAlias, updateCanonicalAliasResult = updateCanonicalAliasResult, publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult, @@ -314,6 +318,7 @@ class EditRoomAddressPresenterTest { with(awaitItem()) { eventSink(EditRoomAddressEvents.Save) } + assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Loading::class.java) with(awaitItem()) { assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) eventSink(EditRoomAddressEvents.DismissError) diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt index 0f453be2bcc..73068bcaaf4 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt @@ -27,10 +27,12 @@ import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import kotlin.time.Duration.Companion.seconds @ExperimentalCoroutinesApi class IncomingVerificationPresenterTest { @@ -61,6 +63,9 @@ class IncomingVerificationPresenterTest { isWaiting = false, ) ) + + advanceTimeBy(1.seconds) + resetLambda.assertions().isCalledOnce().with(value(false)) acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acceptVerificationRequestLambda.assertions().isNeverCalled() @@ -69,7 +74,9 @@ class IncomingVerificationPresenterTest { skipItems(1) val initialWaitingState = awaitItem() assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue() - advanceUntilIdle() + + advanceTimeBy(1.seconds) + acceptVerificationRequestLambda.assertions().isCalledOnce() // Remote sent the data fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) @@ -218,6 +225,9 @@ class IncomingVerificationPresenterTest { isWaiting = false, ) ) + + advanceTimeBy(1.seconds) + resetLambda.assertions().isCalledOnce().with(value(false)) acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acceptVerificationRequestLambda.assertions().isNeverCalled() @@ -226,7 +236,9 @@ class IncomingVerificationPresenterTest { skipItems(1) val initialWaitingState = awaitItem() assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue() - advanceUntilIdle() + + advanceTimeBy(1.seconds) + acceptVerificationRequestLambda.assertions().isCalledOnce() // Remote sent the data fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 619d0b77dfb..f7d72b68a38 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -174,7 +174,7 @@ jsoup = "org.jsoup:jsoup:1.19.1" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0" 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_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/SuspendLazy.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/SuspendLazy.kt new file mode 100644 index 00000000000..e5442c49407 --- /dev/null +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/SuspendLazy.kt @@ -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 suspendLazy(coroutineContext: CoroutineContext = EmptyCoroutineContext, block: suspend () -> T): Lazy> { + return lazy(LazyThreadSafetyMode.NONE) { + val deferred = CompletableDeferred() + CoroutineScope(coroutineContext).launch { + deferred.complete(block()) + } + deferred + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/ViewRoomExt.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/ViewRoomExt.kt index ebded7fbe97..c221b2881e1 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/ViewRoomExt.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/ViewRoomExt.kt @@ -18,8 +18,8 @@ fun MatrixRoom.toAnalyticsViewRoom( val activeSpace = selectedSpace?.toActiveSpace() ?: ViewRoom.ActiveSpace.Home return ViewRoom( - isDM = isDirect, - isSpace = isSpace, + isDM = info().isDirect, + isSpace = info().isSpace, trigger = trigger, activeSpace = activeSpace, viaKeyboard = viaKeyboard @@ -27,5 +27,5 @@ fun MatrixRoom.toAnalyticsViewRoom( } 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 } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt index 86c9e4e825b..79576e0adb8 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt @@ -8,34 +8,12 @@ package io.element.android.libraries.matrix.api.auth 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 * reauthentication. */ 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 * account. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/platform/InitPlatformService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/platform/InitPlatformService.kt new file mode 100644 index 00000000000..6199d1704d2 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/platform/InitPlatformService.kt @@ -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) +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 41b25c0dc0e..ec94ec0b92b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -45,21 +45,10 @@ import java.io.File interface MatrixRoom : Closeable { val sessionId: SessionId val roomId: RoomId - val displayName: String - val canonicalAlias: RoomAlias? - val alternativeAliases: List - 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 roomInfoFlow: Flow + val roomInfoFlow: StateFlow val roomTypingMembersFlow: Flow> val identityStateChangesFlow: Flow> @@ -72,7 +61,7 @@ interface MatrixRoom : Closeable { * 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). */ - val isOneToOne: Boolean get() = activeMemberCount == 2L + val isOneToOne: Boolean get() = info().activeMembersCount == 2L /** * The current loaded members as a StateFlow. @@ -83,6 +72,11 @@ interface MatrixRoom : Closeable { val roomNotificationSettingsStateFlow: StateFlow + /** + * 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. */ @@ -453,4 +447,6 @@ interface MatrixRoom : Closeable { * Update the join rule for this room. */ suspend fun updateJoinRule(joinRule: JoinRule): Result + + suspend fun getUpdatedIsEncrypted(): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt index 32cc4646c09..ad526c6b404 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt @@ -27,7 +27,9 @@ data class MatrixRoomInfo( val rawName: String?, val topic: String?, val avatarUrl: String?, + val isPublic: Boolean, val isDirect: Boolean, + val isEncrypted: Boolean?, val joinRule: JoinRule?, val isSpace: Boolean, val isTombstoned: Boolean, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomIsDmCheck.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomIsDmCheck.kt index b1c9787bfa2..3dd54db9b1a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomIsDmCheck.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomIsDmCheck.kt @@ -7,6 +7,8 @@ package io.element.android.libraries.matrix.api.room +import kotlinx.coroutines.flow.first + /** * 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). @@ -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. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/MatrixRoomAlias.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/MatrixRoomAlias.kt index f9158ae3601..25adbdf3beb 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/MatrixRoomAlias.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/MatrixRoomAlias.kt @@ -19,7 +19,7 @@ fun MatrixRoom.matches(roomIdOrAlias: RoomIdOrAlias): Boolean { roomIdOrAlias.roomId == roomId } is RoomIdOrAlias.Alias -> { - roomIdOrAlias.roomAlias == canonicalAlias || roomIdOrAlias.roomAlias in alternativeAliases + roomIdOrAlias.roomAlias == info().canonicalAlias || roomIdOrAlias.roomAlias in info().alternativeAliases } } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt index 3e1d77fd3cf..973b5cca0d2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt @@ -32,7 +32,7 @@ suspend fun MatrixClient.getRecentDirectRooms( getRecentlyVisitedRooms().getOrNull()?.let { roomIds -> roomIds .mapNotNull { roomId -> getRoom(roomId) } - .filter { it.isDm && it.isJoined() } + .filter { it.isDm() && it.isJoined() } .map { room -> val otherUser = room.getMembers().getOrNull() ?.firstOrNull { it.userId != sessionId } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/TimelineItemEventOrigin.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/TimelineItemEventOrigin.kt index 10835f04482..528e28d5cf4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/TimelineItemEventOrigin.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/TimelineItemEventOrigin.kt @@ -10,5 +10,6 @@ package io.element.android.libraries.matrix.api.timeline.item.event enum class TimelineItemEventOrigin { LOCAL, SYNC, - PAGINATION + PAGINATION, + CACHE, } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt index 53ee37ca78e..5cef6cde02b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt @@ -10,6 +10,5 @@ package io.element.android.libraries.matrix.api.tracing import timber.log.Timber interface TracingService { - fun setupTracing(tracingConfiguration: TracingConfiguration) fun createTimberTree(target: String): Timber.Tree } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 1f08be27b39..45d6188f0f6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -151,8 +151,7 @@ class RustMatrixClient( private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(innerSyncService) private val innerNotificationClient = runBlocking { innerClient.notificationClient(notificationProcessSetup) } private val notificationService = RustNotificationService(innerNotificationClient, dispatchers, clock) - private val notificationSettingsService = RustNotificationSettingsService(innerClient, dispatchers) - .apply { start() } + private val notificationSettingsService = RustNotificationSettingsService(innerClient, sessionCoroutineScope, dispatchers) private val encryptionService = RustEncryptionService( client = innerClient, syncService = rustSyncService, @@ -216,7 +215,7 @@ class RustMatrixClient( userId = sessionId, // TODO cache for displayName? displayName = null, - avatarUrl = innerClient.cachedAvatarUrl(), + avatarUrl = null, ) ) @@ -237,6 +236,9 @@ class RustMatrixClient( sessionDelegate.bindClient(this) sessionCoroutineScope.launch { + // Start notification settings + notificationSettingsService.start() + // Force a refresh of the profile getUserProfile() } @@ -479,10 +481,10 @@ class RustMatrixClient( appCoroutineScope.launch { roomFactory.destroy() rustSyncService.destroy() + notificationSettingsService.destroy() } sessionCoroutineScope.cancel() clientDelegateTaskHandle?.cancelAndDestroy() - notificationSettingsService.destroy() verificationService.destroy() sessionDelegate.clearCurrentClient() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt index f98f9fedc82..1e76e69fa47 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt @@ -9,6 +9,8 @@ package io.element.android.libraries.matrix.impl.analytics 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.isDm +import kotlinx.coroutines.flow.first private fun Long.toAnalyticsRoomSize(): JoinedRoom.RoomSize { 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( - isDM = isDirect, - isSpace = isSpace, - roomSize = joinedMemberCount.toAnalyticsRoomSize(), + isDM = roomInfo.isDm, + isSpace = roomInfo.isSpace, + roomSize = roomInfo.joinedMembersCount.toAnalyticsRoomSize(), trigger = trigger ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcPrompt.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcPrompt.kt index ea1eb93b293..0ba9eb4c498 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcPrompt.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcPrompt.kt @@ -12,10 +12,7 @@ import org.matrix.rustcomponents.sdk.OidcPrompt as RustOidcPrompt internal fun OidcPrompt.toRustPrompt(): RustOidcPrompt { return when (this) { - OidcPrompt.None -> RustOidcPrompt.None - OidcPrompt.Login -> RustOidcPrompt.Login - OidcPrompt.Consent -> RustOidcPrompt.Consent - OidcPrompt.SelectAccount -> RustOidcPrompt.SelectAccount + OidcPrompt.Login -> RustOidcPrompt.Unknown("consent") OidcPrompt.Create -> RustOidcPrompt.Create is OidcPrompt.Unknown -> RustOidcPrompt.Unknown(value) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt index b08a6675a45..5cfd9741ddc 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt @@ -8,10 +8,12 @@ package io.element.android.libraries.matrix.impl.notificationsettings 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.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationSettings +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow @@ -24,9 +26,10 @@ import timber.log.Timber class RustNotificationSettingsService( client: Client, + sessionCoroutineScope: CoroutineScope, private val dispatchers: CoroutineDispatchers, ) : NotificationSettingsService { - private val notificationSettings = client.getNotificationSettings() + private val notificationSettings by suspendLazy(sessionCoroutineScope.coroutineContext + dispatchers.io) { client.getNotificationSettings() } private val _notificationSettingsChangeFlow = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) override val notificationSettingsChangeFlow: SharedFlow = _notificationSettingsChangeFlow.asSharedFlow() @@ -36,22 +39,22 @@ class RustNotificationSettingsService( } } - fun start() { - notificationSettings.setDelegate(notificationSettingsDelegate) + suspend fun start() { + notificationSettings.await().setDelegate(notificationSettingsDelegate) } - fun destroy() { - notificationSettings.setDelegate(null) + suspend fun destroy() { + notificationSettings.await().setDelegate(null) } override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result = 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 = runCatching { - notificationSettings.getDefaultRoomNotificationMode(isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::mapMode) + notificationSettings.await().getDefaultRoomNotificationMode(isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::mapMode) } override suspend fun setDefaultRoomNotificationMode( @@ -61,7 +64,7 @@ class RustNotificationSettingsService( ): Result = withContext(dispatchers.io) { runCatching { try { - notificationSettings.setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode)) + notificationSettings.await().setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode)) } catch (exception: NotificationSettingsException.RuleNotFound) { // `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 @@ -72,13 +75,13 @@ class RustNotificationSettingsService( override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result = withContext(dispatchers.io) { 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 = withContext(dispatchers.io) { 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) { runCatching { - notificationSettings.unmuteRoom(roomId.value, isEncrypted, isOneToOne) + notificationSettings.await().unmuteRoom(roomId.value, isEncrypted, isOneToOne) } } override suspend fun isRoomMentionEnabled(): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.isRoomMentionEnabled() + notificationSettings.await().isRoomMentionEnabled() } } override suspend fun setRoomMentionEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.setRoomMentionEnabled(enabled) + notificationSettings.await().setRoomMentionEnabled(enabled) } } override suspend fun isCallEnabled(): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.isCallEnabled() + notificationSettings.await().isCallEnabled() } } override suspend fun setCallEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.setCallEnabled(enabled) + notificationSettings.await().setCallEnabled(enabled) } } override suspend fun isInviteForMeEnabled(): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.isInviteForMeEnabled() + notificationSettings.await().isInviteForMeEnabled() } } override suspend fun setInviteForMeEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.setInviteForMeEnabled(enabled) + notificationSettings.await().setInviteForMeEnabled(enabled) } } override suspend fun getRoomsWithUserDefinedRules(): Result> = runCatching { - notificationSettings.getRoomsWithUserDefinedRules(enabled = true) + notificationSettings.await().getRoomsWithUserDefinedRules(enabled = true) } override suspend fun canHomeServerPushEncryptedEventsToDevice(): Result = runCatching { - notificationSettings.canPushEncryptedEventToDevice() + notificationSettings.await().canPushEncryptedEventToDevice() } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/platform/RustInitPlatformService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/platform/RustInitPlatformService.kt new file mode 100644 index 00000000000..1324ebe7965 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/platform/RustInitPlatformService.kt @@ -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 + ) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt index 5b39595e203..de72a54469e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt @@ -23,6 +23,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentMap import org.matrix.rustcomponents.sdk.Membership 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.RoomInfo as RustRoomInfo import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode @@ -36,7 +37,13 @@ class MatrixRoomInfoMapper { rawName = it.rawName, topic = it.topic, avatarUrl = it.avatarUrl, + isPublic = it.isPublic, isDirect = it.isDirect, + isEncrypted = when (it.encryptionState) { + EncryptionState.ENCRYPTED -> true + EncryptionState.NOT_ENCRYPTED -> false + EncryptionState.UNKNOWN -> null + }, joinRule = it.joinRule?.map(), isSpace = it.isSpace, isTombstoned = it.isTombstoned, @@ -63,6 +70,44 @@ class MatrixRoomInfoMapper { 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) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index d44e8a3c3a2..5b742e83f86 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -73,6 +73,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine @@ -82,13 +83,13 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.DateDividerMode import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener import org.matrix.rustcomponents.sdk.KnockRequestsListener import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfoListener -import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType import org.matrix.rustcomponents.sdk.TimelineConfiguration import org.matrix.rustcomponents.sdk.TimelineFilter @@ -101,6 +102,7 @@ import org.matrix.rustcomponents.sdk.getElementCallRequiredPermissions import org.matrix.rustcomponents.sdk.use import timber.log.Timber import uniffi.matrix_sdk.RoomPowerLevelChanges +import uniffi.matrix_sdk_base.EncryptionState import java.io.File import kotlin.coroutines.cancellation.CancellationException import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange @@ -112,7 +114,6 @@ import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline class RustMatrixRoom( override val sessionId: SessionId, private val deviceId: DeviceId, - private val roomListItem: RoomListItem, private val innerRoom: InnerRoom, innerTimeline: InnerTimeline, private val notificationSettingsService: NotificationSettingsService, @@ -124,22 +125,17 @@ class RustMatrixRoom( private val matrixRoomInfoMapper: MatrixRoomInfoMapper, private val featureFlagService: FeatureFlagService, private val roomMembershipObserver: RoomMembershipObserver, + initialRoomInfo: MatrixRoomInfo, ) : MatrixRoom { override val roomId = RoomId(innerRoom.id()) - override val roomInfoFlow: Flow = mxCallbackFlow { - runCatching { innerRoom.roomInfo() } - .getOrNull() - ?.let(matrixRoomInfoMapper::map) - ?.let { initial -> - channel.trySend(initial) - } + override val roomInfoFlow: StateFlow = mxCallbackFlow { innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener { override fun call(roomInfo: RoomInfo) { channel.trySend(matrixRoomInfoMapper.map(roomInfo)) } }) - } + }.stateIn(sessionCoroutineScope, started = SharingStarted.Lazily, initialValue = initialRoomInfo) override val roomTypingMembersFlow: Flow> = mxCallbackFlow { val initial = emptyList() @@ -307,39 +303,6 @@ class RustMatrixRoom( 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 - 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() { val useCache = membersStateFlow.value is MatrixRoomMembersState.Unknown val source = if (useCache) { @@ -377,6 +340,7 @@ class RustMatrixRoom( val currentRoomNotificationSettings = currentState.roomNotificationSettings() _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings) runCatching { + val isEncrypted = roomInfoFlow.value.isEncrypted ?: getUpdatedIsEncrypted().getOrThrow() notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() }.map { _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(it) @@ -866,6 +830,12 @@ class RustMatrixRoom( } } + override suspend fun getUpdatedIsEncrypted(): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.latestEncryptionState() == EncryptionState.ENCRYPTED + } + } + private fun createTimeline( timeline: InnerTimeline, mode: Timeline.Mode, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt index 4488b2c036b..2bcd15c142a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt @@ -104,10 +104,10 @@ class RustRoomFactory( return@withContext null } val liveTimeline = roomReferences.fullRoom.timeline() + val initialRoomInfo = roomReferences.fullRoom.roomInfo() RustMatrixRoom( sessionId = sessionId, deviceId = deviceId, - roomListItem = roomReferences.roomListItem, innerRoom = roomReferences.fullRoom, innerTimeline = liveTimeline, sessionCoroutineScope = sessionCoroutineScope, @@ -119,6 +119,7 @@ class RustRoomFactory( matrixRoomInfoMapper = matrixRoomInfoMapper, featureFlagService = featureFlagService, roomMembershipObserver = roomMembershipObserver, + initialRoomInfo = matrixRoomInfoMapper.map(initialRoomInfo), ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 3bd3699037a..46c2bfebd48 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -56,6 +56,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.launchIn @@ -214,19 +215,19 @@ class RustTimeline( _timelineItems, backPaginationStatus, forwardPaginationStatus, - matrixRoom.roomInfoFlow.map { it.creator }, + matrixRoom.roomInfoFlow.map { it.creator to it.isDm }.distinctUntilChanged(), isTimelineInitialized, ) { timelineItems, backwardPaginationStatus, forwardPaginationStatus, - roomCreator, + (roomCreator, isDm), isTimelineInitialized -> withContext(dispatcher) { timelineItems .let { items -> roomBeginningPostProcessor.process( items = items, - isDm = matrixRoom.isDm, + isDm = isDm, roomCreator = roomCreator, hasMoreToLoadBackwards = backwardPaginationStatus.hasMoreToLoad, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index 3e2817bf9ba..89661b5888e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -152,6 +152,7 @@ private fun RustEventItemOrigin.map(): TimelineItemEventOrigin { RustEventItemOrigin.LOCAL -> TimelineItemEventOrigin.LOCAL RustEventItemOrigin.SYNC -> TimelineItemEventOrigin.SYNC RustEventItemOrigin.PAGINATION -> TimelineItemEventOrigin.PAGINATION + RustEventItemOrigin.CACHE -> TimelineItemEventOrigin.CACHE } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt index 74fb2e9db10..0638e499747 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt @@ -20,17 +20,6 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) 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 { 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(), +) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt index c6fc62348e2..887155e5d70 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt @@ -42,7 +42,7 @@ fun TestScope.createRustMatrixClientFactory( ) = RustMatrixClientFactory( baseDirectory = baseDirectory, cacheDirectory = cacheDirectory, - appCoroutineScope = this, + appCoroutineScope = backgroundScope, coroutineDispatchers = testCoroutineDispatchers(), sessionStore = sessionStore, userAgentProvider = SimpleUserAgentProvider(), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt index 0af84fc6e3b..43bae9a208e 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt @@ -38,7 +38,7 @@ class RustMatrixClientTest { innerClient = FakeRustClient(), baseDirectory = File(""), sessionStore = sessionStore, - appCoroutineScope = this, + appCoroutineScope = backgroundScope, sessionDelegate = aRustClientSessionDelegate( sessionStore = sessionStore, ), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExtKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExtKtTest.kt index bce9097c5f4..1ecfb672b81 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExtKtTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExtKtTest.kt @@ -9,13 +9,14 @@ package io.element.android.libraries.matrix.impl.analytics import com.google.common.truth.Truth.assertThat 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.aRoomInfo +import kotlinx.coroutines.test.runTest import org.junit.Test class JoinedRoomExtKtTest { @Test - fun `test room size mapping`() { + fun `test room size mapping`() = runTest { mapOf( listOf(0L, 1L) to JoinedRoom.RoomSize.One, listOf(2L, 2L) to JoinedRoom.RoomSize.Two, @@ -39,7 +40,7 @@ class JoinedRoomExtKtTest { } @Test - fun `test isDirect parameter mapping`() { + fun `test isDirect parameter mapping`() = runTest { assertThat(aMatrixRoom(isDirect = true).toAnalyticsJoinedRoom(null)) .isEqualTo( JoinedRoom( @@ -52,7 +53,7 @@ class JoinedRoomExtKtTest { } @Test - fun `test isSpace parameter mapping`() { + fun `test isSpace parameter mapping`() = runTest { assertThat(aMatrixRoom(isSpace = true).toAnalyticsJoinedRoom(null)) .isEqualTo( JoinedRoom( @@ -65,8 +66,8 @@ class JoinedRoomExtKtTest { } @Test - fun `test trigger parameter mapping`() { - assertThat(aMatrixRoom().toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) + fun `test trigger parameter mapping`() = runTest { + assertThat(aMatrixRoom(isDirect = false, isSpace = false, joinedMemberCount = 1).toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) .isEqualTo( JoinedRoom( isDM = false, @@ -81,11 +82,9 @@ class JoinedRoomExtKtTest { isDirect: Boolean = false, isSpace: Boolean = false, joinedMemberCount: Long = 0 - ): MatrixRoom { - return FakeMatrixRoom( - isDirect = isDirect, - isSpace = isSpace, - joinedMemberCount = joinedMemberCount - ) + ): FakeMatrixRoom { + return FakeMatrixRoom().apply { + givenRoomInfo(aRoomInfo(isDirect = isDirect, isSpace = isSpace, joinedMembersCount = joinedMemberCount)) + } } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomInfo.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomInfo.kt index f5341a1c5e8..15445183c0d 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomInfo.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomInfo.kt @@ -17,6 +17,7 @@ import org.matrix.rustcomponents.sdk.RoomHistoryVisibility import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomNotificationMode +import uniffi.matrix_sdk_base.EncryptionState fun aRustRoomInfo( id: String = A_ROOM_ID.value, @@ -24,6 +25,7 @@ fun aRustRoomInfo( rawName: String? = A_ROOM_NAME, topic: String? = null, avatarUrl: String? = null, + encryptionState: EncryptionState = EncryptionState.UNKNOWN, isDirect: Boolean = false, isPublic: Boolean = false, isSpace: Boolean = false, @@ -57,6 +59,7 @@ fun aRustRoomInfo( rawName = rawName, topic = topic, avatarUrl = avatarUrl, + encryptionState = encryptionState, isDirect = isDirect, isPublic = isPublic, isSpace = isSpace, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClient.kt index 0a9983d60f7..55dd385b6a1 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClient.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClient.kt @@ -35,11 +35,11 @@ class FakeRustClient( override fun userId(): String = userId override fun deviceId(): String = deviceId 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 session(): Session = session 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 fun syncService(): SyncServiceBuilder = FakeRustSyncServiceBuilder() override fun roomDirectorySearch(): RoomDirectorySearch = FakeRustRoomDirectorySearch() diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsServiceTest.kt index 83461b97506..34b8c114b62 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsServiceTest.kt @@ -37,6 +37,7 @@ class RustNotificationSettingsServiceTest { client = FakeRustClient( notificationSettings = notificationSettings, ), + sessionCoroutineScope = this, dispatchers = testCoroutineDispatchers(), ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt index 1befd53ae0e..33ce5c8c333 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt @@ -33,6 +33,7 @@ import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toPersistentList import org.junit.Test 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.RoomHistoryVisibility as RustRoomHistoryVisibility import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode @@ -48,6 +49,7 @@ class MatrixRoomInfoMapperTest { rawName = "rawName", topic = "topic", avatarUrl = AN_AVATAR_URL, + encryptionState = EncryptionState.ENCRYPTED, isDirect = true, isPublic = false, isSpace = false, @@ -84,7 +86,9 @@ class MatrixRoomInfoMapperTest { rawName = "rawName", topic = "topic", avatarUrl = AN_AVATAR_URL, + isPublic = false, isDirect = true, + isEncrypted = true, isSpace = false, isTombstoned = false, isFavorite = false, @@ -130,6 +134,7 @@ class MatrixRoomInfoMapperTest { rawName = null, topic = null, avatarUrl = null, + encryptionState = EncryptionState.UNKNOWN, isDirect = false, isPublic = true, joinRule = null, @@ -165,6 +170,8 @@ class MatrixRoomInfoMapperTest { rawName = null, topic = null, avatarUrl = null, + isEncrypted = null, + isPublic = true, isDirect = false, joinRule = null, isSpace = false, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt index ede0b4b728e..9806cbb1cb9 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt @@ -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.FakeMatrixClient 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.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -32,7 +33,9 @@ class DefaultJoinRoomTest { val roomSummary = aRoomSummary() val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomSummary) } - val roomResult = FakeMatrixRoom() + val roomResult = FakeMatrixRoom().apply { + givenRoomInfo(aRoomInfo()) + } val aTrigger = JoinedRoom.Trigger.MobilePermalink val client: MatrixClient = FakeMatrixClient().also { it.joinRoomLambda = joinRoomLambda @@ -67,7 +70,9 @@ class DefaultJoinRoomTest { val roomSummary = aRoomSummary() val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomSummary) } - val roomResult = FakeMatrixRoom() + val roomResult = FakeMatrixRoom().apply { + givenRoomInfo(aRoomInfo()) + } val aTrigger = JoinedRoom.Trigger.MobilePermalink val client: MatrixClient = FakeMatrixClient().also { it.joinRoomLambda = joinRoomLambda @@ -103,7 +108,9 @@ class DefaultJoinRoomTest { val roomSummary = aRoomSummary() val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomSummary) } - val roomResult = FakeMatrixRoom() + val roomResult = FakeMatrixRoom().apply { + givenRoomInfo(aRoomInfo()) + } val aTrigger = JoinedRoom.Trigger.MobilePermalink val client: MatrixClient = FakeMatrixClient().also { it.joinRoomLambda = joinRoomLambda diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 04c32b5445d..7d9bbd1bfc8 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -7,6 +7,7 @@ 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.EventId import io.element.android.libraries.matrix.api.core.ProgressCallback @@ -66,19 +67,9 @@ import java.io.File class FakeMatrixRoom( override val sessionId: SessionId = A_SESSION_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 = 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(), override val liveTimeline: Timeline = FakeTimeline(), + initialRoomInfo: MatrixRoomInfo = aRoomInfo(), override val roomCoroutineScope: CoroutineScope = TestScope(), private var roomPermalinkResult: () -> Result = { lambdaError() }, private var eventPermalinkResult: (EventId) -> Result = { lambdaError() }, @@ -156,8 +147,8 @@ class FakeMatrixRoom( private val enableEncryptionResult: () -> Result = { lambdaError() }, private val updateJoinRuleResult: (JoinRule) -> Result = { lambdaError() }, ) : MatrixRoom { - private val _roomInfoFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) - override val roomInfoFlow: Flow = _roomInfoFlow + private val _roomInfoFlow: MutableStateFlow = MutableStateFlow(initialRoomInfo) + override val roomInfoFlow: StateFlow = _roomInfoFlow fun givenRoomInfo(roomInfo: MatrixRoomInfo) { _roomInfoFlow.tryEmit(roomInfo) @@ -200,14 +191,14 @@ class FakeMatrixRoom( } override suspend fun updateRoomNotificationSettings(): Result = simulateLongTask { - val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() + val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, info().isEncrypted.orFalse(), isOneToOne).getOrThrow() roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(notificationSettings) return Result.success(Unit) } override suspend fun enableEncryption(): Result = simulateLongTask { enableEncryptionResult().onSuccess { - isEncrypted = true + givenRoomInfo(info().copy(isEncrypted = true)) emitSyncUpdate() } } @@ -616,6 +607,10 @@ class FakeMatrixRoom( updateJoinRuleResult(joinRule) } + override suspend fun getUpdatedIsEncrypted(): Result = simulateLongTask { + Result.success(info().isEncrypted.orFalse()) + } + fun givenRoomMembersState(state: MatrixRoomMembersState) { membersStateFlow.value = state } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt index 50a4c30a853..cc56c2d32fb 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt @@ -33,7 +33,9 @@ fun aRoomInfo( rawName: String? = A_ROOM_RAW_NAME, topic: String? = A_ROOM_TOPIC, avatarUrl: String? = AN_AVATAR_URL, + isPublic: Boolean = true, isDirect: Boolean = false, + isEncrypted: Boolean = false, joinRule: JoinRule? = JoinRule.Public, isSpace: Boolean = false, isTombstoned: Boolean = false, @@ -65,7 +67,9 @@ fun aRoomInfo( rawName = rawName, topic = topic, avatarUrl = avatarUrl, + isPublic = isPublic, isDirect = isDirect, + isEncrypted = isEncrypted, joinRule = joinRule, isSpace = isSpace, isTombstoned = isTombstoned, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index c85fe824d5f..ac561c57d25 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -46,7 +46,9 @@ fun aRoomSummary( rawName: String? = A_ROOM_RAW_NAME, topic: String? = A_ROOM_TOPIC, avatarUrl: String? = null, + isPublic: Boolean = true, isDirect: Boolean = false, + isEncrypted: Boolean = false, joinRule: JoinRule? = JoinRule.Public, isSpace: Boolean = false, isTombstoned: Boolean = false, @@ -80,7 +82,9 @@ fun aRoomSummary( rawName = rawName, topic = topic, avatarUrl = avatarUrl, + isPublic = isPublic, isDirect = isDirect, + isEncrypted = isEncrypted, joinRule = joinRule, isSpace = isSpace, isTombstoned = isTombstoned, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt index 8ab1a4b9f50..eb281f09d61 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt @@ -40,11 +40,12 @@ fun getRoomMemberAsState(roomMembersState: MatrixRoomMembersState, userId: UserI @Composable fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): State { val roomMembers = roomMembersState.roomMembers() - return remember(roomMembersState) { + val roomInfo by roomInfoFlow.collectAsState() + return remember(roomMembersState, roomInfo.isDirect) { derivedStateOf { roomMembers ?.filter { it.membership.isActive() } - ?.takeIf { it.size == 2 && isDirect } + ?.takeIf { it.size == 2 && roomInfo.isDirect == true } ?.find { it.userId != sessionId } } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt index f7f47b78945..e820004b83c 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt @@ -67,9 +67,9 @@ fun MatrixRoom.canPinUnpin(updateKey: Long): State { } @Composable -fun MatrixRoom.isDmAsState(updateKey: Long): State { - return produceState(initialValue = false, key1 = updateKey) { - value = isDm +fun MatrixRoom.isDmAsState(): State { + return produceState(initialValue = false) { + roomInfoFlow.collect { value = it.isDm } } } @@ -105,25 +105,25 @@ fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State { @Composable fun MatrixRoom.isOwnUserAdmin(): Boolean { - val roomInfo by roomInfoFlow.collectAsState(initial = null) - val powerLevel = roomInfo?.userPowerLevels?.get(sessionId) ?: 0L + val roomInfo by roomInfoFlow.collectAsState() + val powerLevel = roomInfo.userPowerLevels[sessionId] ?: 0L return RoomMember.Role.forPowerLevel(powerLevel) == RoomMember.Role.ADMIN } @Composable fun MatrixRoom.rawName(): String? { - val roomInfo by roomInfoFlow.collectAsState(initial = null) - return roomInfo?.rawName + val roomInfo by roomInfoFlow.collectAsState() + return roomInfo.rawName } @Composable fun MatrixRoom.topic(): String? { - val roomInfo by roomInfoFlow.collectAsState(initial = null) - return roomInfo?.topic + val roomInfo by roomInfoFlow.collectAsState() + return roomInfo.topic } @Composable fun MatrixRoom.avatarUrl(): String? { - val roomInfo by roomInfoFlow.collectAsState(initial = null) - return roomInfo?.avatarUrl + val roomInfo by roomInfoFlow.collectAsState() + return roomInfo.avatarUrl } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChange.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChange.kt index 2549bba2d9a..7610b55b20d 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChange.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChange.kt @@ -30,10 +30,10 @@ import kotlinx.coroutines.flow.onEach @OptIn(ExperimentalCoroutinesApi::class) fun MatrixRoom.roomMemberIdentityStateChange(): Flow> { - return syncUpdateFlow + return roomInfoFlow .filter { // Room cannot become unencrypted, so we can just apply a filter here. - isEncrypted + it.isEncrypted == true } .distinctUntilChanged() .flatMapLatest { diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembersTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembersTest.kt index 4187fe6e904..c54795486f0 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembersTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembersTest.kt @@ -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_3 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 kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest @@ -31,8 +32,10 @@ class MatrixRoomMembersTest { fun `getDirectRoomMember emits other member for encrypted DM with 2 joined members`() = runTest { val matrixRoom = FakeMatrixRoom( sessionId = A_USER_ID, - isEncrypted = true, - isDirect = true, + initialRoomInfo = aRoomInfo( + isDirect = true, + joinedMembersCount = 2, + ) ) moleculeFlow(RecompositionMode.Immediate) { matrixRoom.getDirectRoomMember( @@ -47,8 +50,7 @@ class MatrixRoomMembersTest { fun `getDirectRoomMember emit null if the room is not a dm`() = runTest { val matrixRoom = FakeMatrixRoom( sessionId = A_USER_ID, - isEncrypted = true, - isDirect = false, + initialRoomInfo = aRoomInfo(isDirect = false) ) moleculeFlow(RecompositionMode.Immediate) { matrixRoom.getDirectRoomMember( @@ -63,8 +65,10 @@ class MatrixRoomMembersTest { fun `getDirectRoomMember emits other member even if the room is not encrypted`() = runTest { val matrixRoom = FakeMatrixRoom( sessionId = A_USER_ID, - isEncrypted = false, - isDirect = true, + initialRoomInfo = aRoomInfo( + isDirect = true, + activeMembersCount = 2, + ) ) moleculeFlow(RecompositionMode.Immediate) { matrixRoom.getDirectRoomMember( @@ -79,8 +83,7 @@ class MatrixRoomMembersTest { fun `getDirectRoomMember emit null if the room has only 1 member`() = runTest { val matrixRoom = FakeMatrixRoom( sessionId = A_USER_ID, - isEncrypted = true, - isDirect = true, + initialRoomInfo = aRoomInfo(isDirect = true) ) moleculeFlow(RecompositionMode.Immediate) { matrixRoom.getDirectRoomMember( @@ -95,9 +98,9 @@ class MatrixRoomMembersTest { fun `getDirectRoomMember emit null if the room has only 3 members`() = runTest { val matrixRoom = FakeMatrixRoom( sessionId = A_USER_ID, - isEncrypted = true, - isDirect = true, - ) + ).apply { + givenRoomInfo(aRoomInfo(isDirect = true)) + } moleculeFlow(RecompositionMode.Immediate) { matrixRoom.getDirectRoomMember( MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2, roomMember3)) @@ -111,8 +114,7 @@ class MatrixRoomMembersTest { fun `getDirectRoomMember emit null if the other member is not active`() = runTest { val matrixRoom = FakeMatrixRoom( sessionId = A_USER_ID, - isEncrypted = true, - isDirect = true, + initialRoomInfo = aRoomInfo(isDirect = true), ) moleculeFlow(RecompositionMode.Immediate) { matrixRoom.getDirectRoomMember( @@ -132,8 +134,10 @@ class MatrixRoomMembersTest { fun `getDirectRoomMember emit the other member if there are 2 active members`() = runTest { val matrixRoom = FakeMatrixRoom( sessionId = A_USER_ID, - isEncrypted = true, - isDirect = true, + initialRoomInfo = aRoomInfo( + isDirect = true, + activeMembersCount = 2, + ) ) moleculeFlow(RecompositionMode.Immediate) { matrixRoom.getDirectRoomMember( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt index 6617a8b9cbc..f79157e8586 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt @@ -64,7 +64,7 @@ class MediaGalleryPresenter @AssistedInject constructor( val coroutineScope = rememberCoroutineScope() 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.Hidden) } @@ -139,7 +139,7 @@ class MediaGalleryPresenter @AssistedInject constructor( } return MediaGalleryState( - roomName = roomInfo?.name ?: room.displayName, + roomName = roomInfo.name.orEmpty(), mode = mode, groupedMediaItems = groupedMediaItems, mediaBottomSheetState = mediaBottomSheetState, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt index 81e270a09f3..56396ca2a60 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt @@ -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.media.FakeMatrixMediaLoader 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.mediaviewer.impl.datasource.FakeMediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource @@ -51,7 +52,7 @@ class MediaGalleryPresenterTest { startLambda = startLambda, ), room = FakeMatrixRoom( - displayName = A_ROOM_NAME, + initialRoomInfo = aRoomInfo(name = A_ROOM_NAME), createTimelineResult = { Result.success(FakeTimeline()) }, ) ) @@ -70,7 +71,7 @@ class MediaGalleryPresenterTest { fun `present - change mode`() = runTest { val presenter = createMediaGalleryPresenter( room = FakeMatrixRoom( - displayName = A_ROOM_NAME, + initialRoomInfo = aRoomInfo(name = A_ROOM_NAME), createTimelineResult = { Result.success(FakeTimeline()) }, ) ) @@ -100,7 +101,7 @@ class MediaGalleryPresenterTest { val presenter = createMediaGalleryPresenter( room = FakeMatrixRoom( sessionId = A_USER_ID, - displayName = A_ROOM_NAME, + initialRoomInfo = aRoomInfo(name = A_ROOM_NAME), createTimelineResult = { Result.success(FakeTimeline()) }, canRedactOwnResult = { Result.success(canDeleteOwn) } ) @@ -143,7 +144,7 @@ class MediaGalleryPresenterTest { val presenter = createMediaGalleryPresenter( room = FakeMatrixRoom( sessionId = A_USER_ID, - displayName = A_ROOM_NAME, + initialRoomInfo = aRoomInfo(name = A_ROOM_NAME), createTimelineResult = { Result.success(FakeTimeline()) }, canRedactOtherResult = { Result.success(canDeleteOther) } ) @@ -176,7 +177,7 @@ class MediaGalleryPresenterTest { fun `present - delete bottom sheet`() = runTest { val presenter = createMediaGalleryPresenter( room = FakeMatrixRoom( - displayName = A_ROOM_NAME, + initialRoomInfo = aRoomInfo(name = A_ROOM_NAME), createTimelineResult = { Result.success(FakeTimeline()) }, ) ) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt index 070f8769733..aea52c8b782 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt @@ -152,8 +152,8 @@ class NotificationBroadcastReceiverHandler @Inject constructor( imageUriString = null, imageMimeType = null, threadId = threadId, - roomName = room.displayName, - roomIsDm = room.isDm, + roomName = room.info().name, + roomIsDm = room.isDm(), outGoingMessage = true, ) onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt index 17093fef43c..0ce6038d798 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt @@ -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.core.aBuildMeta 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.timeline.FakeTimeline import io.element.android.libraries.preferences.api.store.SessionPreferencesStore @@ -337,7 +338,14 @@ class NotificationBroadcastReceiverHandlerTest { val matrixRoom = FakeMatrixRoom( liveTimeline = liveTimeline, getUpdatedMemberResult = { Result.success(aRoomMember()) }, - ) + ).apply { + givenRoomInfo( + aRoomInfo( + isDirect = true, + activeMembersCount = 2, + ) + ) + } val onNotifiableEventReceivedResult = lambdaRecorder { _ -> } val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult) val sut = createNotificationBroadcastReceiverHandler( @@ -396,7 +404,14 @@ class NotificationBroadcastReceiverHandlerTest { val matrixRoom = FakeMatrixRoom( liveTimeline = liveTimeline, getUpdatedMemberResult = { Result.success(aRoomMember()) }, - ) + ).apply { + givenRoomInfo( + aRoomInfo( + isDirect = true, + activeMembersCount = 2, + ) + ) + } val onNotifiableEventReceivedResult = lambdaRecorder { _ -> } val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult) val sut = createNotificationBroadcastReceiverHandler(