Do not offer to start a call in a DM with no other member

A DM whose other member has left still offered the call button. Starting
that call put the user alone in a room nobody could join, with no way out
of the call screen.

The presenter now clears canStartCall for a DM with no other active
member. activeMembersCount counts joined and invited members, so a fresh
DM whose peer has been invited but has not joined yet can still start a
call. Joining an already ongoing call is untouched, so a call started
before the other member left stays reachable.

Relates to https://github.com/element-hq/element-x-android/issues/6189
This commit is contained in:
hayaksi1
2026-08-09 04:08:56 +03:00
parent 044737a3a7
commit 717efe46c5
2 changed files with 48 additions and 4 deletions
@@ -60,10 +60,18 @@ class RoomCallStatePresenter(
isUserLocallyInTheCall = isUserLocallyInTheCall,
isAudioCall = roomInfo.activeCallIntentConsensus.isAudio(),
)
else -> RoomCallState.StandBy(
canStartCall = canJoinCall,
isDM = roomInfo.isDm
)
else -> {
// Nobody would ever join a call started in a DM whose other member has left,
// and the call screen has no way out of that state.
// activeMembersCount counts joined and invited members, so a DM whose peer
// has been invited but has not joined yet can still start a call.
// See https://github.com/element-hq/element-x-android/issues/6189
val isEmptyDm = roomInfo.isDm && roomInfo.activeMembersCount <= 1
RoomCallState.StandBy(
canStartCall = canJoinCall && !isEmptyDm,
isDM = roomInfo.isDm
)
}
}
}
}
@@ -49,6 +49,42 @@ class RoomCallStatePresenterTest {
}
}
@Test
fun `present - a DM with no other member cannot start a call`() = runTest {
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
initialRoomInfo = aRoomInfo(isDm = true, activeMembersCount = 1, joinedMembersCount = 1),
roomPermissions = roomPermissions(true),
)
)
val presenter = createRoomCallStatePresenter(joinedRoom = room)
presenter.test {
skipItems(1)
assertThat(awaitItem()).isEqualTo(
RoomCallState.StandBy(
canStartCall = false,
isDM = true
)
)
}
}
@Test
fun `present - a DM becomes callable once the other member is active`() = runTest {
val baseRoom = FakeBaseRoom(
initialRoomInfo = aRoomInfo(isDm = true, activeMembersCount = 1, joinedMembersCount = 1),
roomPermissions = roomPermissions(true),
)
val room = FakeJoinedRoom(baseRoom = baseRoom)
val presenter = createRoomCallStatePresenter(joinedRoom = room)
presenter.test {
skipItems(1)
assertThat(awaitItem()).isEqualTo(RoomCallState.StandBy(canStartCall = false, isDM = true))
baseRoom.givenRoomInfo(aRoomInfo(isDm = true, activeMembersCount = 2, joinedMembersCount = 2))
assertThat(awaitItem()).isEqualTo(RoomCallState.StandBy(canStartCall = true, isDM = true))
}
}
@Test
fun `present - element call not available`() = runTest {
val room = FakeJoinedRoom(