Adapt to new DM definition changes in the SDK (#6748)

* Set `DmRoomDefinition.TwoPeople` in `ClientBuilder`. This applies the 'direct and with at most 2 non-service members' rule to what the SDK should consider a DM.

* Map `RoomInfo.isDm` from the SDK

* Map `NotificationData.isDm` from `NotificationInfo.roomInfo.isDm`

* Remove `RoomIsDmCheck` file as its extension functions are now redundant. Move `Room.isDm` helper function to `BaseRoom`.

* Map `isDm` in `SpaceRoom` from the SDK too

* Replace `isDirect` with `isDm` where possible

* Map `RoomMember.isServiceMember` from the SDK and use it to tell apart normal members of a room from service members (i.e. `RoomMembersState.getDirectRoomMember`)
This commit is contained in:
Jorge Martin Espinosa 2026-05-11 17:22:16 +02:00 committed by GitHub
parent a38b7a61ad
commit 4975f61eb8
66 changed files with 115 additions and 232 deletions

View file

@ -14,7 +14,6 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.api.ActiveRoomsHolder

View file

@ -19,7 +19,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
import io.element.android.libraries.matrix.api.room.CallIntentConsensus import io.element.android.libraries.matrix.api.room.CallIntentConsensus
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData

View file

@ -11,7 +11,6 @@ package io.element.android.features.invite.api
import android.os.Parcelable import android.os.Parcelable
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize

View file

@ -540,7 +540,7 @@ internal class DefaultInvitePeoplePresenterTest {
} }
@Test @Test
fun `present - suggestions are loaded from recent direct rooms`() = runTest { fun `present - suggestions are loaded from recent DM rooms`() = runTest {
val dmRoomId = RoomId("!dm_room:server.org") val dmRoomId = RoomId("!dm_room:server.org")
val otherUserId = UserId("@frank:server.org") val otherUserId = UserId("@frank:server.org")
val matrixClient = FakeMatrixClient(sessionId = A_USER_ID).apply { val matrixClient = FakeMatrixClient(sessionId = A_USER_ID).apply {
@ -554,7 +554,7 @@ internal class DefaultInvitePeoplePresenterTest {
roomId = dmRoomId, roomId = dmRoomId,
initialRoomInfo = aRoomInfo( initialRoomInfo = aRoomInfo(
id = dmRoomId, id = dmRoomId,
isDirect = true, isDm = true,
activeMembersCount = 2, activeMembersCount = 2,
currentUserMembership = CurrentUserMembership.JOINED, currentUserMembership = CurrentUserMembership.JOINED,
), ),
@ -591,7 +591,7 @@ internal class DefaultInvitePeoplePresenterTest {
roomId = dmRoomId, roomId = dmRoomId,
initialRoomInfo = aRoomInfo( initialRoomInfo = aRoomInfo(
id = dmRoomId, id = dmRoomId,
isDirect = true, isDm = true,
activeMembersCount = 2, activeMembersCount = 2,
currentUserMembership = CurrentUserMembership.JOINED, currentUserMembership = CurrentUserMembership.JOINED,
), ),

View file

@ -44,7 +44,6 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo

View file

@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope

View file

@ -115,7 +115,7 @@ class LeaveBaseRoomPresenterTest {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
result = FakeBaseRoom().apply { result = FakeBaseRoom().apply {
givenRoomInfo(aRoomInfo(isDirect = true, activeMembersCount = 2)) givenRoomInfo(aRoomInfo(isDm = true, activeMembersCount = 2))
}, },
) )
} }

View file

@ -79,7 +79,6 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId

View file

@ -57,7 +57,6 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
import io.element.android.libraries.matrix.api.room.getDirectRoomMember import io.element.android.libraries.matrix.api.room.getDirectRoomMember
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.use import io.element.android.libraries.matrix.api.room.powerlevels.use
import io.element.android.libraries.matrix.api.timeline.TimelineException import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId

View file

@ -190,6 +190,7 @@ internal fun SuggestionsPickerViewPreview() {
isIgnored = false, isIgnored = false,
role = RoomMember.Role.User, role = RoomMember.Role.User,
membershipChangeReason = null, membershipChangeReason = null,
isServiceMember = false,
) )
val anAlias = remember { RoomAlias("#room:domain.org") } val anAlias = remember { RoomAlias("#room:domain.org") }
SuggestionsPickerView( SuggestionsPickerView(

View file

@ -44,7 +44,6 @@ import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings

View file

@ -49,7 +49,6 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.asEventId
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.ReceiptType

View file

@ -598,7 +598,7 @@ class MessagesPresenterTest {
baseRoom = FakeBaseRoom( baseRoom = FakeBaseRoom(
roomPermissions = roomPermissions(), roomPermissions = roomPermissions(),
).apply { ).apply {
givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 1, activeMembersCount = 1)) givenRoomInfo(aRoomInfo(isDm = true, joinedMembersCount = 1, activeMembersCount = 1))
}, },
typingNoticeResult = { Result.success(Unit) }, typingNoticeResult = { Result.success(Unit) },
) )
@ -1112,7 +1112,7 @@ class MessagesPresenterTest {
canRedactOwn = true, canRedactOwn = true,
canPinUnpin = true, canPinUnpin = true,
), ),
initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true) initialRoomInfo = aRoomInfo(isDm = true, isEncrypted = true)
).apply { ).apply {
givenRoomMembersState(RoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2)))) givenRoomMembersState(RoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2))))
}, },

View file

@ -1066,7 +1066,7 @@ class MessageComposerPresenterTest {
) )
givenRoomInfo( givenRoomInfo(
aRoomInfo( aRoomInfo(
isDirect = true, isDm = true,
activeMembersCount = 2, activeMembersCount = 2,
) )
) )

View file

@ -223,7 +223,7 @@ class NotificationSettingsPresenter(
notificationSettingsService.setDefaultRoomNotificationMode( notificationSettingsService.setDefaultRoomNotificationMode(
isEncrypted = encryptedGroupDefaultMode != RoomNotificationMode.ALL_MESSAGES, isEncrypted = encryptedGroupDefaultMode != RoomNotificationMode.ALL_MESSAGES,
mode = RoomNotificationMode.ALL_MESSAGES, mode = RoomNotificationMode.ALL_MESSAGES,
isOneToOne = false, isDM = false,
) )
} }
@ -234,7 +234,7 @@ class NotificationSettingsPresenter(
notificationSettingsService.setDefaultRoomNotificationMode( notificationSettingsService.setDefaultRoomNotificationMode(
isEncrypted = encryptedOneToOneDefaultMode != RoomNotificationMode.ALL_MESSAGES, isEncrypted = encryptedOneToOneDefaultMode != RoomNotificationMode.ALL_MESSAGES,
mode = RoomNotificationMode.ALL_MESSAGES, mode = RoomNotificationMode.ALL_MESSAGES,
isOneToOne = true, isDM = true,
) )
} }
}.fold( }.fold(

View file

@ -34,12 +34,12 @@ class EditDefaultNotificationSettingNode(
} }
data class Inputs( data class Inputs(
val isOneToOne: Boolean val isDm: Boolean
) : NodeInputs ) : NodeInputs
private val callback: Callback = callback() private val callback: Callback = callback()
private val inputs = inputs<Inputs>() private val inputs = inputs<Inputs>()
private val presenter = presenterFactory.create(inputs.isOneToOne) private val presenter = presenterFactory.create(inputs.isDm)
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {

View file

@ -42,12 +42,12 @@ import kotlin.time.Duration.Companion.seconds
@AssistedInject @AssistedInject
class EditDefaultNotificationSettingPresenter( class EditDefaultNotificationSettingPresenter(
private val notificationSettingsService: NotificationSettingsService, private val notificationSettingsService: NotificationSettingsService,
@Assisted private val isOneToOne: Boolean, @Assisted private val isDm: Boolean,
private val roomListService: RoomListService, private val roomListService: RoomListService,
) : Presenter<EditDefaultNotificationSettingState> { ) : Presenter<EditDefaultNotificationSettingState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(isOneToOne: Boolean): EditDefaultNotificationSettingPresenter fun create(isDm: Boolean): EditDefaultNotificationSettingPresenter
} }
private val collator = Collator.getInstance().apply { private val collator = Collator.getInstance().apply {
@ -86,7 +86,7 @@ class EditDefaultNotificationSettingPresenter(
} }
return EditDefaultNotificationSettingState( return EditDefaultNotificationSettingState(
isOneToOne = isOneToOne, isOneToOne = isDm,
mode = mode.value, mode = mode.value,
roomsWithUserDefinedMode = roomsWithUserDefinedMode.value.toImmutableList(), roomsWithUserDefinedMode = roomsWithUserDefinedMode.value.toImmutableList(),
changeNotificationSettingAction = changeNotificationSettingAction.value, changeNotificationSettingAction = changeNotificationSettingAction.value,
@ -96,7 +96,7 @@ class EditDefaultNotificationSettingPresenter(
} }
private fun CoroutineScope.fetchSettings(mode: MutableState<RoomNotificationMode?>) = launch { private fun CoroutineScope.fetchSettings(mode: MutableState<RoomNotificationMode?>) = launch {
mode.value = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = isOneToOne).getOrThrow() mode.value = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = isDm).getOrThrow()
} }
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
@ -129,7 +129,7 @@ class EditDefaultNotificationSettingPresenter(
val roomWithUserDefinedRules: Set<RoomId> = notificationSettingsService.getRoomsWithUserDefinedRules().getOrDefault(emptyList()).toSet() val roomWithUserDefinedRules: Set<RoomId> = notificationSettingsService.getRoomsWithUserDefinedRules().getOrDefault(emptyList()).toSet()
roomsWithUserDefinedMode.value = summaries roomsWithUserDefinedMode.value = summaries
.filter { roomSummary -> .filter { roomSummary ->
roomWithUserDefinedRules.contains(roomSummary.roomId) && roomSummary.isOneToOne == isOneToOne roomWithUserDefinedRules.contains(roomSummary.roomId) && roomSummary.isDm == isDm
} }
.map { roomSummary -> .map { roomSummary ->
EditNotificationSettingRoomInfo( EditNotificationSettingRoomInfo(
@ -154,9 +154,9 @@ class EditDefaultNotificationSettingPresenter(
private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<AsyncAction<Unit>>) = launch { private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<AsyncAction<Unit>>) = launch {
action.runUpdatingStateNoSuccess { action.runUpdatingStateNoSuccess {
// On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did). // On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did).
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne) notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isDM = isDm)
.map { .map {
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne) notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isDM = isDm)
} }
} }
} }

View file

@ -198,7 +198,7 @@ class EditDefaultNotificationSettingsPresenterTest {
): EditDefaultNotificationSettingPresenter { ): EditDefaultNotificationSettingPresenter {
return EditDefaultNotificationSettingPresenter( return EditDefaultNotificationSettingPresenter(
notificationSettingsService = notificationSettingsService, notificationSettingsService = notificationSettingsService,
isOneToOne = false, isDm = false,
roomListService = roomListService, roomListService = roomListService,
) )
} }

View file

@ -61,8 +61,8 @@ class NotificationSettingsPresenterTest {
val notificationSettingsService = FakeNotificationSettingsService() val notificationSettingsService = FakeNotificationSettingsService()
val presenter = createNotificationSettingsPresenter(notificationSettingsService) val presenter = createNotificationSettingsPresenter(notificationSettingsService)
presenter.test { presenter.test {
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES) notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, isDM = false, mode = RoomNotificationMode.ALL_MESSAGES)
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES) notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, isDM = false, mode = RoomNotificationMode.ALL_MESSAGES)
val updatedState = consumeItemsUntilPredicate { val updatedState = consumeItemsUntilPredicate {
(it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid) (it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)
?.defaultGroupNotificationMode == RoomNotificationMode.ALL_MESSAGES ?.defaultGroupNotificationMode == RoomNotificationMode.ALL_MESSAGES
@ -79,12 +79,12 @@ class NotificationSettingsPresenterTest {
presenter.test { presenter.test {
notificationSettingsService.setDefaultRoomNotificationMode( notificationSettingsService.setDefaultRoomNotificationMode(
isEncrypted = true, isEncrypted = true,
isOneToOne = false, isDM = false,
mode = RoomNotificationMode.ALL_MESSAGES mode = RoomNotificationMode.ALL_MESSAGES
) )
notificationSettingsService.setDefaultRoomNotificationMode( notificationSettingsService.setDefaultRoomNotificationMode(
isEncrypted = false, isEncrypted = false,
isOneToOne = false, isDM = false,
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
) )
val updatedState = consumeItemsUntilPredicate { val updatedState = consumeItemsUntilPredicate {

View file

@ -23,7 +23,6 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.CallIntentConsensus import io.element.android.libraries.matrix.api.room.CallIntentConsensus
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.canCall import io.element.android.libraries.matrix.api.room.powerlevels.canCall
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState

View file

@ -89,10 +89,10 @@ class RoomCallStatePresenterTest {
} }
@Test @Test
fun `present - initial state - when is direct room`() = runTest { fun `present - initial state - when is DM room`() = runTest {
val room = FakeJoinedRoom( val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom( baseRoom = FakeBaseRoom(
initialRoomInfo = aRoomInfo(isDirect = true), initialRoomInfo = aRoomInfo(isDm = true),
roomPermissions = roomPermissions(true), roomPermissions = roomPermissions(true),
) )
) )

View file

@ -42,7 +42,6 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
@ -53,7 +52,6 @@ import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -145,7 +143,7 @@ class RoomDetailsPresenter(
} }
RoomDetailsEvent.UnmuteNotification -> { RoomDetailsEvent.UnmuteNotification -> {
scope.launch(dispatchers.io) { scope.launch(dispatchers.io) {
notificationSettingsService.unmuteRoom(room.roomId, isEncrypted, room.isOneToOne) notificationSettingsService.unmuteRoom(room.roomId, isEncrypted, room.isDm())
} }
} }
is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite) is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite)
@ -184,7 +182,7 @@ class RoomDetailsPresenter(
isFavorite = isFavorite, isFavorite = isFavorite,
displayRolesAndPermissionsSettings = !isDm && permissions.canEditRolesAndPermissions, displayRolesAndPermissionsSettings = !isDm && permissions.canEditRolesAndPermissions,
isPublic = joinRule == JoinRule.Public, isPublic = joinRule == JoinRule.Public,
heroes = roomInfo.heroes.toImmutableList(), heroes = roomInfo.heroes,
pinnedMessagesCount = pinnedMessagesCount, pinnedMessagesCount = pinnedMessagesCount,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
canShowKnockRequests = canShowKnockRequests, canShowKnockRequests = canShowKnockRequests,

View file

@ -74,6 +74,7 @@ fun aDmRoomMember(
isIgnored: Boolean = false, isIgnored: Boolean = false,
role: RoomMember.Role = RoomMember.Role.User, role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null, membershipChangeReason: String? = null,
isServiceMember: Boolean = false,
) = RoomMember( ) = RoomMember(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,
@ -83,7 +84,8 @@ fun aDmRoomMember(
powerLevel = powerLevel, powerLevel = powerLevel,
isIgnored = isIgnored, isIgnored = isIgnored,
role = role, role = role,
membershipChangeReason = membershipChangeReason membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember,
) )
fun aRoomDetailsState( fun aRoomDetailsState(

View file

@ -128,6 +128,7 @@ fun aRoomMember(
isIgnored: Boolean = false, isIgnored: Boolean = false,
role: RoomMember.Role = RoomMember.Role.User, role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null, membershipChangeReason: String? = null,
isServiceMember: Boolean = false,
) = RoomMember( ) = RoomMember(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,
@ -138,6 +139,7 @@ fun aRoomMember(
isIgnored = isIgnored, isIgnored = isIgnored,
role = role, role = role,
membershipChangeReason = membershipChangeReason, membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember,
) )
fun aRoomMemberList() = persistentListOf( fun aRoomMemberList() = persistentListOf(

View file

@ -160,7 +160,7 @@ class RoomNotificationSettingsPresenter(
suspend { suspend {
val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
pendingModeState.value = null pendingModeState.value = null
notificationSettingsService.getRoomNotificationSettings(room.roomId, isEncrypted, room.isOneToOne).getOrThrow() notificationSettingsService.getRoomNotificationSettings(room.roomId, isEncrypted, room.isDm()).getOrThrow()
}.runCatchingUpdatingState(roomNotificationSettings) }.runCatchingUpdatingState(roomNotificationSettings)
} }
@ -170,7 +170,7 @@ class RoomNotificationSettingsPresenter(
val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode( defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode(
isEncrypted, isEncrypted,
room.isOneToOne room.isDm()
).getOrThrow() ).getOrThrow()
} }

View file

@ -199,7 +199,7 @@ class RoomDetailsPresenterTest {
givenRoomInfo( givenRoomInfo(
aRoomInfo( aRoomInfo(
isEncrypted = true, isEncrypted = true,
isDirect = true, isDm = true,
) )
) )
} }
@ -284,7 +284,7 @@ class RoomDetailsPresenterTest {
givenRoomInfo( givenRoomInfo(
aRoomInfo( aRoomInfo(
isEncrypted = true, isEncrypted = true,
isDirect = true, isDm = true,
) )
) )
} }
@ -307,7 +307,6 @@ class RoomDetailsPresenterTest {
val myRoomMember = aRoomMember(A_SESSION_ID) val myRoomMember = aRoomMember(A_SESSION_ID)
val otherRoomMember = aRoomMember(A_USER_ID_2) val otherRoomMember = aRoomMember(A_USER_ID_2)
val room = aJoinedRoom( val room = aJoinedRoom(
isDirect = true,
topic = null, topic = null,
roomPermissions = roomPermissions(), roomPermissions = roomPermissions(),
userDisplayNameResult = { Result.success(A_USER_NAME) }, userDisplayNameResult = { Result.success(A_USER_NAME) },
@ -325,7 +324,7 @@ class RoomDetailsPresenterTest {
givenRoomInfo( givenRoomInfo(
aRoomInfo( aRoomInfo(
isDirect = true, isDm = true,
activeMembersCount = 2, activeMembersCount = 2,
topic = null, topic = null,
) )

View file

@ -15,7 +15,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.recent.getRecentlyVisitedRoomInfoFlow import io.element.android.libraries.matrix.api.room.recent.getRecentlyVisitedRoomInfoFlow
import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListFilter

View file

@ -67,7 +67,7 @@ class LeaveSpacePresenter(
.orEmpty() .orEmpty()
.partition { it.spaceRoom.roomId == leaveSpaceHandle.id } .partition { it.spaceRoom.roomId == leaveSpaceHandle.id }
// By default select all rooms that can be left // By default select all rooms that can be left
val otherRoomsExcludingDm = otherRooms.filter { it.spaceRoom.isDirect != true } val otherRoomsExcludingDm = otherRooms.filter { it.spaceRoom.isDm != true }
selectedRoomIds = otherRoomsExcludingDm selectedRoomIds = otherRoomsExcludingDm
.filter { it.isLastOwner.not() } .filter { it.isLastOwner.not() }
.map { it.spaceRoom.roomId } .map { it.spaceRoom.roomId }

View file

@ -109,6 +109,7 @@ private fun aSpaceInfo(
avatarUrl = null, avatarUrl = null,
isPublic = true, isPublic = true,
isDirect = false, isDirect = false,
isDm = false,
isEncrypted = false, isEncrypted = false,
joinRule = joinRule, joinRule = joinRule,
isSpace = true, isSpace = true,

View file

@ -98,13 +98,13 @@ class LeaveSpacePresenterTest {
listOf( listOf(
aLeaveSpaceRoom(spaceRoom = aSpace), aLeaveSpaceRoom(spaceRoom = aSpace),
aLeaveSpaceRoom( aLeaveSpaceRoom(
spaceRoom = aSpaceRoom(roomId = A_ROOM_ID, isDirect = false) spaceRoom = aSpaceRoom(roomId = A_ROOM_ID, isDm = false)
), ),
aLeaveSpaceRoom( aLeaveSpaceRoom(
spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_2, isDirect = true) spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_2, isDm = true)
), ),
aLeaveSpaceRoom( aLeaveSpaceRoom(
spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_3, isDirect = null) spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_3, isDm = null)
), ),
) )
) )

View file

@ -19,7 +19,7 @@ fun BaseRoom.toAnalyticsViewRoom(
val activeSpace = selectedSpace?.toActiveSpace() ?: ViewRoom.ActiveSpace.Home val activeSpace = selectedSpace?.toActiveSpace() ?: ViewRoom.ActiveSpace.Home
return ViewRoom( return ViewRoom(
isDM = info().isDirect, isDM = info().isDm,
isSpace = info().isSpace, isSpace = info().isSpace,
trigger = trigger, trigger = trigger,
activeSpace = activeSpace, activeSpace = activeSpace,

View file

@ -21,7 +21,7 @@ interface NotificationSettingsService {
val notificationSettingsChangeFlow: SharedFlow<Unit> val notificationSettingsChangeFlow: SharedFlow<Unit>
suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationSettings> suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationSettings>
suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationMode> suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationMode>
suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result<Unit> suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isDM: Boolean): Result<Unit>
suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result<Unit> suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result<Unit>
suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result<Unit> suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result<Unit>
suspend fun muteRoom(roomId: RoomId): Result<Unit> suspend fun muteRoom(roomId: RoomId): Result<Unit>

View file

@ -61,13 +61,12 @@ interface BaseRoom : Closeable {
*/ */
fun info(): RoomInfo = roomInfoFlow.value fun info(): RoomInfo = roomInfoFlow.value
fun predecessorRoom(): PredecessorRoom?
/** /**
* A one-to-one is a room with exactly 2 members. * Returns whether the [BaseRoom] is a DM, with an updated state from the latest [RoomInfo].
* See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules).
*/ */
val isOneToOne: Boolean get() = info().activeMembersCount == 2L fun isDm() = roomInfoFlow.value.isDm
fun predecessorRoom(): PredecessorRoom?
/** /**
* Try to load the room members and update the membersFlow. * Try to load the room members and update the membersFlow.

View file

@ -29,6 +29,7 @@ data class RoomInfo(
val avatarUrl: String?, val avatarUrl: String?,
val isPublic: Boolean?, val isPublic: Boolean?,
val isDirect: Boolean, val isDirect: Boolean,
val isDm: Boolean,
val isEncrypted: Boolean?, val isEncrypted: Boolean?,
val joinRule: JoinRule?, val joinRule: JoinRule?,
val isSpace: Boolean, val isSpace: Boolean,

View file

@ -1,32 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.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).
*
* @param isDirect true if the room is direct
* @param activeMembersCount the number of active members in the room (joined or invited)
*/
fun isDm(isDirect: Boolean, activeMembersCount: Int): Boolean {
return isDirect && activeMembersCount <= 2
}
/**
* Returns whether the [BaseRoom] is a DM, with an updated state from the latest [RoomInfo].
*/
suspend fun BaseRoom.isDm() = roomInfoFlow.first().isDm
/**
* Returns whether the [RoomInfo] is from a DM.
*/
val RoomInfo.isDm get() = isDm(isDirect, activeMembersCount.toInt())

View file

@ -22,6 +22,7 @@ data class RoomMember(
val isIgnored: Boolean, val isIgnored: Boolean,
val role: Role, val role: Role,
val membershipChangeReason: String?, val membershipChangeReason: String?,
val isServiceMember: Boolean,
) { ) {
/** /**
* Role of the RoomMember, based on its [powerLevel]. * Role of the RoomMember, based on its [powerLevel].

View file

@ -40,5 +40,5 @@ fun RoomMembersState.activeRoomMembers(): List<RoomMember> {
fun RoomMembersState.getDirectRoomMember(roomInfo: RoomInfo, sessionId: SessionId): RoomMember? { fun RoomMembersState.getDirectRoomMember(roomInfo: RoomInfo, sessionId: SessionId): RoomMember? {
return roomMembers() return roomMembers()
?.takeIf { roomInfo.isDm } ?.takeIf { roomInfo.isDm }
?.find { it.userId != sessionId && it.membership.isActive() } ?.find { !it.isServiceMember && it.userId != sessionId && it.membership.isActive() }
} }

View file

@ -12,7 +12,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.toMatrixUser import io.element.android.libraries.matrix.api.room.toMatrixUser
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow

View file

@ -21,5 +21,5 @@ data class RoomSummary(
is LatestEventValue.Remote -> latestEvent.timestamp is LatestEventValue.Remote -> latestEvent.timestamp
is LatestEventValue.RoomInvite -> latestEvent.timestamp is LatestEventValue.RoomInvite -> latestEvent.timestamp
} }
val isOneToOne get() = info.activeMembersCount == 2L val isDm = info.isDm
} }

View file

@ -38,6 +38,7 @@ data class SpaceRoom(
*/ */
val via: ImmutableList<String>, val via: ImmutableList<String>,
val isDirect: Boolean?, val isDirect: Boolean?,
val isDm: Boolean?,
) { ) {
val isSpace = roomType == RoomType.Space val isSpace = roomType == RoomType.Space

View file

@ -1,54 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.room
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class RoomIsDmCheckTest {
@Test
fun `a room is a DM only if it has at most 2 members and is direct`() {
val isDirect = true
val activeMembersCount = 2
val isDm = isDm(isDirect, activeMembersCount)
assertThat(isDm).isTrue()
}
@Test
fun `a room can be a DM if it has also a single active user`() {
val isDirect = true
val activeMembersCount = 1
val isDm = isDm(isDirect, activeMembersCount)
assertThat(isDm).isTrue()
}
@Test
fun `a room is not a DM if it's not direct`() {
val isDirect = false
val activeMembersCount = 2
val isDm = isDm(isDirect, activeMembersCount)
assertThat(isDm).isFalse()
}
@Test
fun `a room is not a DM if it has more than 2 active users`() {
val isDirect = true
val activeMembersCount = 3
val isDm = isDm(isDirect, activeMembersCount)
assertThat(isDm).isFalse()
}
}

View file

@ -41,6 +41,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncVersion
import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import uniffi.matrix_sdk_base.DmRoomDefinition
import uniffi.matrix_sdk_base.MediaRetentionPolicy import uniffi.matrix_sdk_base.MediaRetentionPolicy
import uniffi.matrix_sdk_crypto.CollectStrategy import uniffi.matrix_sdk_crypto.CollectStrategy
import uniffi.matrix_sdk_crypto.DecryptionSettings import uniffi.matrix_sdk_crypto.DecryptionSettings
@ -169,6 +170,7 @@ class RustMatrixClientFactory(
) )
.enableShareHistoryOnInvite(true) .enableShareHistoryOnInvite(true)
.threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.Threads), threadSubscriptions = false) .threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.Threads), threadSubscriptions = false)
.dmRoomDefinition(DmRoomDefinition.TWO_MEMBERS)
.requestConfig( .requestConfig(
RequestConfig( RequestConfig(
timeout = 30_000uL, timeout = 30_000uL,

View file

@ -11,7 +11,6 @@ package io.element.android.libraries.matrix.impl.analytics
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.isDm
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
private fun Long.toAnalyticsRoomSize(): JoinedRoom.RoomSize { private fun Long.toAnalyticsRoomSize(): JoinedRoom.RoomSize {

View file

@ -17,7 +17,6 @@ import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.libraries.matrix.impl.room.join.map
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import org.matrix.rustcomponents.sdk.NotificationEvent import org.matrix.rustcomponents.sdk.NotificationEvent
@ -37,10 +36,6 @@ class NotificationMapper(
): Result<NotificationData> { ): Result<NotificationData> {
return runCatchingExceptions { return runCatchingExceptions {
notificationItem.use { item -> notificationItem.use { item ->
val isDm = isDm(
isDirect = item.roomInfo.isDirect,
activeMembersCount = item.roomInfo.joinedMembersCount.toInt(),
)
val timestamp = item.timestamp() ?: clock.epochMillis() val timestamp = item.timestamp() ?: clock.epochMillis()
NotificationData( NotificationData(
sessionId = sessionId, sessionId = sessionId,
@ -50,10 +45,10 @@ class NotificationMapper(
senderAvatarUrl = item.senderInfo.avatarUrl, senderAvatarUrl = item.senderInfo.avatarUrl,
senderDisplayName = item.senderInfo.displayName, senderDisplayName = item.senderInfo.displayName,
senderIsNameAmbiguous = item.senderInfo.isNameAmbiguous, senderIsNameAmbiguous = item.senderInfo.isNameAmbiguous,
roomAvatarUrl = item.roomInfo.avatarUrl ?: item.senderInfo.avatarUrl.takeIf { isDm }, roomAvatarUrl = item.roomInfo.avatarUrl ?: item.senderInfo.avatarUrl.takeIf { item.roomInfo.isDm },
roomDisplayName = item.roomInfo.displayName, roomDisplayName = item.roomInfo.displayName,
isDirect = item.roomInfo.isDirect, isDirect = item.roomInfo.isDirect,
isDm = isDm, isDm = item.roomInfo.isDm,
isSpace = item.roomInfo.isSpace, isSpace = item.roomInfo.isSpace,
isEncrypted = item.roomInfo.isEncrypted.orFalse(), isEncrypted = item.roomInfo.isEncrypted.orFalse(),
isNoisy = item.isNoisy.orFalse(), isNoisy = item.isNoisy.orFalse(),

View file

@ -62,11 +62,11 @@ class RustNotificationSettingsService(
override suspend fun setDefaultRoomNotificationMode( override suspend fun setDefaultRoomNotificationMode(
isEncrypted: Boolean, isEncrypted: Boolean,
mode: RoomNotificationMode, mode: RoomNotificationMode,
isOneToOne: Boolean isDM: Boolean
): Result<Unit> = withContext(dispatchers.io) { ): Result<Unit> = withContext(dispatchers.io) {
runCatchingExceptions { runCatchingExceptions {
try { try {
notificationSettings.await().setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode)) notificationSettings.await().setDefaultRoomNotificationMode(isEncrypted, isDM, mode.let(RoomNotificationSettingsMapper::mapMode))
} catch (exception: NotificationSettingsException.RuleNotFound) { } catch (exception: NotificationSettingsException.RuleNotFound) {
// `setDefaultRoomNotificationMode` updates multiple rules including unstable rules (e.g. the polls push rules defined in the MSC3930) // `setDefaultRoomNotificationMode` updates multiple rules including unstable rules (e.g. the polls push rules defined in the MSC3930)
// since production home servers may not have these rules yet, we drop the RuleNotFound error // since production home servers may not have these rules yet, we drop the RuleNotFound error

View file

@ -348,7 +348,7 @@ class JoinedRustRoom(
roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings) roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings)
runCatchingExceptions { runCatchingExceptions {
val isEncrypted = roomInfoFlow.value.isEncrypted ?: getUpdatedIsEncrypted().getOrThrow() val isEncrypted = roomInfoFlow.value.isEncrypted ?: getUpdatedIsEncrypted().getOrThrow()
notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() notificationSettingsService.getRoomNotificationSettings(roomId = roomId, isEncrypted = isEncrypted, isOneToOne = isDm()).getOrThrow()
}.map { }.map {
roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Ready(it) roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Ready(it)
}.onFailure { }.onFailure {

View file

@ -18,7 +18,7 @@ import org.matrix.rustcomponents.sdk.RoomInfo
*/ */
fun RoomInfo.elementHeroes(): List<MatrixUser> { fun RoomInfo.elementHeroes(): List<MatrixUser> {
return heroes return heroes
.takeIf { isDirect && activeMembersCount.toLong() == 2L } .takeIf { isDm }
?.takeIf { it.size == 1 } ?.takeIf { it.size == 1 }
?.map { it.map() } ?.map { it.map() }
.orEmpty() .orEmpty()

View file

@ -43,6 +43,7 @@ class RoomInfoMapper {
avatarUrl = it.avatarUrl, avatarUrl = it.avatarUrl,
isPublic = it.isPublic, isPublic = it.isPublic,
isDirect = it.isDirect, isDirect = it.isDirect,
isDm = it.isDm,
isEncrypted = when (it.encryptionState) { isEncrypted = when (it.encryptionState) {
EncryptionState.ENCRYPTED -> true EncryptionState.ENCRYPTED -> true
EncryptionState.NOT_ENCRYPTED -> false EncryptionState.NOT_ENCRYPTED -> false

View file

@ -23,7 +23,6 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
@ -119,7 +118,7 @@ class RustBaseRoom(
innerRoom.membersNoSync().use { members -> innerRoom.membersNoSync().use { members ->
members.nextChunk(members.len()) members.nextChunk(members.len())
?.map(RoomMemberMapper::map) ?.map(RoomMemberMapper::map)
?.firstOrNull { roomMember -> roomMember.userId != sessionId && roomMember.membership.isActive() } ?.firstOrNull { roomMember -> !roomMember.isServiceMember && roomMember.userId != sessionId && roomMember.membership.isActive() }
} }
} else { } else {
null null

View file

@ -28,7 +28,8 @@ object RoomMemberMapper {
powerLevel = powerLevel, powerLevel = powerLevel,
isIgnored = roomMember.isIgnored, isIgnored = roomMember.isIgnored,
role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel), role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel),
membershipChangeReason = roomMember.membershipChangeReason membershipChangeReason = roomMember.membershipChangeReason,
isServiceMember = roomMember.isServiceMember,
) )
} }

View file

@ -36,6 +36,7 @@ class SpaceRoomMapper {
worldReadable = spaceRoom.worldReadable.orFalse(), worldReadable = spaceRoom.worldReadable.orFalse(),
via = spaceRoom.via.toImmutableList(), via = spaceRoom.via.toImmutableList(),
isDirect = spaceRoom.isDirect, isDirect = spaceRoom.isDirect,
isDm = spaceRoom.isDm,
) )
} }
} }

View file

@ -20,7 +20,6 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.room.IntentionalMention
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.MsgType import io.element.android.libraries.matrix.api.timeline.MsgType

View file

@ -43,6 +43,19 @@ class JoinedExtKtTest {
@Test @Test
fun `test isDirect parameter mapping`() = runTest { fun `test isDirect parameter mapping`() = runTest {
assertThat(aRoom(isDirect = true).toAnalyticsJoinedRoom(null)) assertThat(aRoom(isDirect = true).toAnalyticsJoinedRoom(null))
.isEqualTo(
JoinedRoom(
isDM = false,
isSpace = false,
roomSize = JoinedRoom.RoomSize.One,
trigger = null
)
)
}
@Test
fun `test isDm parameter mapping`() = runTest {
assertThat(aRoom(isDm = true).toAnalyticsJoinedRoom(null))
.isEqualTo( .isEqualTo(
JoinedRoom( JoinedRoom(
isDM = true, isDM = true,
@ -80,12 +93,13 @@ class JoinedExtKtTest {
} }
private fun aRoom( private fun aRoom(
isDm: Boolean = false,
isDirect: Boolean = false, isDirect: Boolean = false,
isSpace: Boolean = false, isSpace: Boolean = false,
joinedMemberCount: Long = 0 joinedMemberCount: Long = 0
): FakeBaseRoom { ): FakeBaseRoom {
return FakeBaseRoom().apply { return FakeBaseRoom().apply {
givenRoomInfo(aRoomInfo(isDirect = isDirect, isSpace = isSpace, joinedMembersCount = joinedMemberCount)) givenRoomInfo(aRoomInfo(isDm = isDm, isDirect = isDirect, isSpace = isSpace, joinedMembersCount = joinedMemberCount))
} }
} }
} }

View file

@ -50,5 +50,5 @@ internal fun aRustSpaceRoom(
childrenCount = childrenCount, childrenCount = childrenCount,
state = state, state = state,
heroes = heroes, heroes = heroes,
via = emptyList() via = emptyList(),
) )

View file

@ -17,6 +17,7 @@ import org.matrix.rustcomponents.sdk.RequestConfig
import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder
import org.matrix.rustcomponents.sdk.SqliteStoreBuilder import org.matrix.rustcomponents.sdk.SqliteStoreBuilder
import uniffi.matrix_sdk.BackupDownloadStrategy import uniffi.matrix_sdk.BackupDownloadStrategy
import uniffi.matrix_sdk_base.DmRoomDefinition
import uniffi.matrix_sdk_crypto.CollectStrategy import uniffi.matrix_sdk_crypto.CollectStrategy
import uniffi.matrix_sdk_crypto.DecryptionSettings import uniffi.matrix_sdk_crypto.DecryptionSettings
@ -47,5 +48,6 @@ class FakeFfiClientBuilder(
override fun sqliteStore(config: SqliteStoreBuilder): ClientBuilder = this override fun sqliteStore(config: SqliteStoreBuilder): ClientBuilder = this
override fun inMemoryStore(): ClientBuilder = this override fun inMemoryStore(): ClientBuilder = this
override fun crossProcessLockConfig(crossProcessLockConfig: CrossProcessLockConfig): ClientBuilder = this override fun crossProcessLockConfig(crossProcessLockConfig: CrossProcessLockConfig): ClientBuilder = this
override fun dmRoomDefinition(dmRoomDefinition: DmRoomDefinition): ClientBuilder = this
override suspend fun build() = buildResult() override suspend fun build() = buildResult()
} }

View file

@ -20,8 +20,7 @@ class RoomInfoExtTest {
@Test @Test
fun `get non empty element Heroes`() { fun `get non empty element Heroes`() {
val result = aRustRoomInfo( val result = aRustRoomInfo(
isDirect = true, isDm = true,
activeMembersCount = 2uL,
heroes = listOf(aRustRoomHero()) heroes = listOf(aRustRoomHero())
).elementHeroes() ).elementHeroes()
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
@ -38,8 +37,7 @@ class RoomInfoExtTest {
@Test @Test
fun `too many heroes and element Heroes is empty`() { fun `too many heroes and element Heroes is empty`() {
val result = aRustRoomInfo( val result = aRustRoomInfo(
isDirect = true, isDm = true,
activeMembersCount = 2uL,
heroes = listOf(aRustRoomHero(), aRustRoomHero()) heroes = listOf(aRustRoomHero(), aRustRoomHero())
).elementHeroes() ).elementHeroes()
assertThat(result).isEmpty() assertThat(result).isEmpty()
@ -48,18 +46,7 @@ class RoomInfoExtTest {
@Test @Test
fun `not direct and element Heroes is empty`() { fun `not direct and element Heroes is empty`() {
val result = aRustRoomInfo( val result = aRustRoomInfo(
isDirect = false, isDm = false,
activeMembersCount = 2uL,
heroes = listOf(aRustRoomHero())
).elementHeroes()
assertThat(result).isEmpty()
}
@Test
fun `too many members and element Heroes is empty`() {
val result = aRustRoomInfo(
isDirect = true,
activeMembersCount = 3uL,
heroes = listOf(aRustRoomHero()) heroes = listOf(aRustRoomHero())
).elementHeroes() ).elementHeroes()
assertThat(result).isEmpty() assertThat(result).isEmpty()

View file

@ -86,6 +86,7 @@ class RoomInfoMapperTest {
privilegedCreatorsRole = true, privilegedCreatorsRole = true,
isLowPriority = true, isLowPriority = true,
activeRoomCallConsensusIntent = RtcCallIntentConsensus.Full(RtcCallIntent.AUDIO), activeRoomCallConsensusIntent = RtcCallIntentConsensus.Full(RtcCallIntent.AUDIO),
isDm = true,
) )
) )
).isEqualTo( ).isEqualTo(
@ -136,6 +137,7 @@ class RoomInfoMapperTest {
privilegedCreatorRole = true, privilegedCreatorRole = true,
isLowPriority = true, isLowPriority = true,
activeCallIntentConsensus = CallIntentConsensus.Full(CallIntent.AUDIO), activeCallIntentConsensus = CallIntentConsensus.Full(CallIntent.AUDIO),
isDm = true,
) )
) )
} }
@ -181,6 +183,7 @@ class RoomInfoMapperTest {
privilegedCreatorsRole = true, privilegedCreatorsRole = true,
isLowPriority = true, isLowPriority = true,
activeRoomCallConsensusIntent = RtcCallIntentConsensus.None, activeRoomCallConsensusIntent = RtcCallIntentConsensus.None,
isDm = false,
) )
) )
).isEqualTo( ).isEqualTo(
@ -225,6 +228,7 @@ class RoomInfoMapperTest {
privilegedCreatorRole = true, privilegedCreatorRole = true,
isLowPriority = true, isLowPriority = true,
activeCallIntentConsensus = CallIntentConsensus.None, activeCallIntentConsensus = CallIntentConsensus.None,
isDm = false,
) )
) )
} }

View file

@ -70,12 +70,12 @@ class FakeNotificationSettingsService(
} }
} }
override suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result<Unit> { override suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isDM: Boolean): Result<Unit> {
val error = setDefaultNotificationModeError val error = setDefaultNotificationModeError
if (error != null) { if (error != null) {
return Result.failure(error) return Result.failure(error)
} }
if (isOneToOne) { if (isDM) {
if (isEncrypted) { if (isEncrypted) {
defaultEncryptedOneToOneRoomNotificationMode = mode defaultEncryptedOneToOneRoomNotificationMode = mode
} else { } else {

View file

@ -134,7 +134,11 @@ class FakeJoinedRoom(
} }
override suspend fun updateRoomNotificationSettings(): Result<Unit> = simulateLongTask { override suspend fun updateRoomNotificationSettings(): Result<Unit> = simulateLongTask {
val notificationSettings = roomNotificationSettingsService.getRoomNotificationSettings(roomId, info().isEncrypted.orFalse(), isOneToOne).getOrThrow() val notificationSettings = roomNotificationSettingsService.getRoomNotificationSettings(
roomId = roomId,
isEncrypted = info().isEncrypted.orFalse(),
isOneToOne = isDm(),
).getOrThrow()
(roomNotificationSettingsStateFlow as MutableStateFlow).value = RoomNotificationSettingsState.Ready(notificationSettings) (roomNotificationSettingsStateFlow as MutableStateFlow).value = RoomNotificationSettingsState.Ready(notificationSettings)
return Result.success(Unit) return Result.success(Unit)
} }

View file

@ -71,6 +71,7 @@ fun aRoomInfo(
privilegedCreatorRole: Boolean = false, privilegedCreatorRole: Boolean = false,
isLowPriority: Boolean = false, isLowPriority: Boolean = false,
activeCallIntentConsensus: CallIntentConsensus = CallIntentConsensus.None, activeCallIntentConsensus: CallIntentConsensus = CallIntentConsensus.None,
isDm: Boolean = false,
) = RoomInfo( ) = RoomInfo(
id = id, id = id,
name = name, name = name,
@ -109,4 +110,5 @@ fun aRoomInfo(
privilegedCreatorRole = privilegedCreatorRole, privilegedCreatorRole = privilegedCreatorRole,
isLowPriority = isLowPriority, isLowPriority = isLowPriority,
activeCallIntentConsensus = activeCallIntentConsensus, activeCallIntentConsensus = activeCallIntentConsensus,
isDm = isDm,
) )

View file

@ -23,6 +23,7 @@ fun aRoomMember(
isIgnored: Boolean = false, isIgnored: Boolean = false,
role: RoomMember.Role = RoomMember.Role.User, role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null, membershipChangeReason: String? = null,
isServiceMember: Boolean = false,
) = RoomMember( ) = RoomMember(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,
@ -33,6 +34,7 @@ fun aRoomMember(
isIgnored = isIgnored, isIgnored = isIgnored,
role = role, role = role,
membershipChangeReason = membershipChangeReason, membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember,
) )
fun aRoomMemberList() = persistentListOf( fun aRoomMemberList() = persistentListOf(

View file

@ -120,6 +120,7 @@ fun aRoomSummary(
privilegedCreatorRole = privilegedCreatorRole, privilegedCreatorRole = privilegedCreatorRole,
isLowPriority = isLowPriority, isLowPriority = isLowPriority,
activeCallIntentConsensus = activeCallIntentConsensus, activeCallIntentConsensus = activeCallIntentConsensus,
isDm = false,
), ),
latestEvent = latestEvent, latestEvent = latestEvent,
) )

View file

@ -30,13 +30,10 @@ class RoomMembersTest {
private val roomMember3 = aRoomMember(A_USER_ID_3) private val roomMember3 = aRoomMember(A_USER_ID_3)
@Test @Test
fun `getDirectRoomMember emits other member for encrypted DM with 2 joined members`() = runTest { fun `getDirectRoomMember emits other member for encrypted DM`() = runTest {
val joinedRoom = FakeBaseRoom( val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo( initialRoomInfo = aRoomInfo(isDm = true, isEncrypted = true)
isDirect = true,
joinedMembersCount = 2,
)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
joinedRoom.getDirectRoomMember( joinedRoom.getDirectRoomMember(
@ -51,7 +48,7 @@ class RoomMembersTest {
fun `getDirectRoomMember emit null if the room is not a dm`() = runTest { fun `getDirectRoomMember emit null if the room is not a dm`() = runTest {
val joinedRoom = FakeBaseRoom( val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(isDirect = false) initialRoomInfo = aRoomInfo(isDm = false)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
joinedRoom.getDirectRoomMember( joinedRoom.getDirectRoomMember(
@ -66,10 +63,7 @@ class RoomMembersTest {
fun `getDirectRoomMember emits other member even if the room is not encrypted`() = runTest { fun `getDirectRoomMember emits other member even if the room is not encrypted`() = runTest {
val joinedRoom = FakeBaseRoom( val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo( initialRoomInfo = aRoomInfo(isDm = true)
isDirect = true,
activeMembersCount = 2,
)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
joinedRoom.getDirectRoomMember( joinedRoom.getDirectRoomMember(
@ -80,42 +74,11 @@ class RoomMembersTest {
} }
} }
@Test
fun `getDirectRoomMember emit null if the room has only 1 member`() = runTest {
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(isDirect = true)
)
moleculeFlow(RecompositionMode.Immediate) {
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(persistentListOf(roomMember1))
)
}.test {
assertThat(awaitItem().value).isNull()
}
}
@Test
fun `getDirectRoomMember emit null if the room has only 3 members`() = runTest {
val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID,
).apply {
givenRoomInfo(aRoomInfo(isDirect = true, activeMembersCount = 3L))
}
moleculeFlow(RecompositionMode.Immediate) {
joinedRoom.getDirectRoomMember(
RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2, roomMember3))
)
}.test {
assertThat(awaitItem().value).isNull()
}
}
@Test @Test
fun `getDirectRoomMember emit null if the other member is not active`() = runTest { fun `getDirectRoomMember emit null if the other member is not active`() = runTest {
val joinedRoom = FakeBaseRoom( val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo(isDirect = true), initialRoomInfo = aRoomInfo(isDm = true),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
joinedRoom.getDirectRoomMember( joinedRoom.getDirectRoomMember(
@ -135,10 +98,7 @@ class RoomMembersTest {
fun `getDirectRoomMember emit the other member if there are 2 active members`() = runTest { fun `getDirectRoomMember emit the other member if there are 2 active members`() = runTest {
val joinedRoom = FakeBaseRoom( val joinedRoom = FakeBaseRoom(
sessionId = A_USER_ID, sessionId = A_USER_ID,
initialRoomInfo = aRoomInfo( initialRoomInfo = aRoomInfo(isDm = true)
isDirect = true,
activeMembersCount = 2,
)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
joinedRoom.getDirectRoomMember( joinedRoom.getDirectRoomMember(

View file

@ -33,6 +33,7 @@ fun aRoomMember(
isIgnored: Boolean = false, isIgnored: Boolean = false,
role: RoomMember.Role = RoomMember.Role.User, role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null, membershipChangeReason: String? = null,
isServiceMember: Boolean = false,
) = RoomMember( ) = RoomMember(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,
@ -43,6 +44,7 @@ fun aRoomMember(
isIgnored = isIgnored, isIgnored = isIgnored,
role = role, role = role,
membershipChangeReason = membershipChangeReason, membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember,
) )
fun aRoomMemberList() = persistentListOf( fun aRoomMemberList() = persistentListOf(

View file

@ -34,6 +34,7 @@ fun aSpaceRoom(
topic: String? = null, topic: String? = null,
worldReadable: Boolean = false, worldReadable: Boolean = false,
isDirect: Boolean? = null, isDirect: Boolean? = null,
isDm: Boolean? = null,
via: List<String> = emptyList(), via: List<String> = emptyList(),
) = SpaceRoom( ) = SpaceRoom(
rawName = rawName, rawName = rawName,
@ -51,5 +52,6 @@ fun aSpaceRoom(
topic = topic, topic = topic,
worldReadable = worldReadable, worldReadable = worldReadable,
via = via.toImmutableList(), via = via.toImmutableList(),
isDirect = isDirect isDirect = isDirect,
isDm = isDm,
) )

View file

@ -19,7 +19,6 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory