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

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

View file

@ -59,11 +59,14 @@ import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugIn
import io.element.android.libraries.mediaplayer.api.MediaPlayer
import io.element.android.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<Plugin>,
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()

View file

@ -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<String> by remember {
derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
derivedStateOf { roomInfo.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
}
val roomAvatar: AsyncData<AvatarData> by remember {
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<Unit>>(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
)
}

View file

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

View file

@ -38,11 +38,11 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.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,

View file

@ -34,6 +34,7 @@ import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.features.poll.api.actions.SendPollResponseAction
import io.element.android.features.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<EventId?>(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,
)
}

View file

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

View file

@ -19,6 +19,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.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 {

View file

@ -63,6 +63,7 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
import io.element.android.libraries.matrix.test.permalink.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(