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

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

View file

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

View file

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

View file

@ -27,7 +27,10 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.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<IdentityStateChange?>(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) {

View file

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

View file

@ -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<AsyncData<RoomNotificationSettings>>
) = 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<RoomNotificationMode?>
) = launch {
val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode(
room.isEncrypted,
isEncrypted,
room.isOneToOne
).getOrThrow()
}

View file

@ -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<UserId>, role: RoomMember.Role): Int {
return this?.userPowerLevels.orEmpty().count { (userId, level) ->
private fun MatrixRoomInfo.userCountWithRole(userIds: List<UserId>, role: RoomMember.Role): Int {
return this.userPowerLevels.count { (userId, level) ->
RoomMember.Role.forPowerLevel(level) == role && userId in userIds
}
}

View file

@ -106,10 +106,10 @@ class ChangeRolesPresenter @AssistedInject constructor(
val hasPendingChanges = usersWithRole.value != selectedUsers.value
val 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
}

View file

@ -57,7 +57,7 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor(
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(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<Boolean>>(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,
)
}
}

View file

@ -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>(RoomAddressValidity.Unknown)
}
val savedRoomAddress = remember { room.firstAliasMatching(homeserverName)?.addressName() }
val savedRoomAddress by remember { derivedStateOf { roomInfo.firstAliasMatching(homeserverName)?.addressName() } }
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
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