Set a default power level to join calls in room (#1927)

* Set a default power level to join calls.

Also, create new rooms taking this power level into account.

* Modify test to make sure we display the disabled state even when there is an ongoing call

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2023-11-30 10:47:48 +01:00 committed by GitHub
parent b94ee9fb0a
commit 0a91184e4a
12 changed files with 98 additions and 21 deletions

View file

@ -0,0 +1 @@
Set a default power level to join calls. Also, create new rooms taking this power level into account.

View file

@ -79,6 +79,7 @@ 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.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.matrix.ui.room.canRedactAsState import io.element.android.libraries.matrix.ui.room.canRedactAsState
@ -108,6 +109,7 @@ class MessagesPresenter @AssistedInject constructor(
private val featureFlagsService: FeatureFlagService, private val featureFlagsService: FeatureFlagService,
@Assisted private val navigator: MessagesNavigator, @Assisted private val navigator: MessagesNavigator,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val currentSessionIdHolder: CurrentSessionIdHolder,
) : Presenter<MessagesState> { ) : Presenter<MessagesState> {
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator) private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
@ -144,6 +146,16 @@ class MessagesPresenter @AssistedInject constructor(
mutableStateOf(false) mutableStateOf(false)
} }
var canJoinCall by rememberSaveable {
mutableStateOf(false)
}
LaunchedEffect(currentSessionIdHolder.current) {
withContext(dispatchers.io) {
canJoinCall = room.canUserJoinCall(userId = currentSessionIdHolder.current).getOrDefault(false)
}
}
val inviteProgress = remember { mutableStateOf<Async<Unit>>(Async.Uninitialized) } val inviteProgress = remember { mutableStateOf<Async<Unit>>(Async.Uninitialized) }
var showReinvitePrompt by remember { mutableStateOf(false) } var showReinvitePrompt by remember { mutableStateOf(false) }
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) { LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) {
@ -162,8 +174,6 @@ class MessagesPresenter @AssistedInject constructor(
val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true) val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
var enableVoiceMessages by remember { mutableStateOf(false) } var enableVoiceMessages by remember { mutableStateOf(false) }
// TODO add min power level to use this feature in the future?
val enableInRoomCalls = true
LaunchedEffect(featureFlagsService) { LaunchedEffect(featureFlagsService) {
enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages) enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages)
} }
@ -193,6 +203,12 @@ class MessagesPresenter @AssistedInject constructor(
} }
} }
val callState = when {
!canJoinCall -> RoomCallState.DISABLED
roomInfo?.hasRoomCall == true -> RoomCallState.ONGOING
else -> RoomCallState.ENABLED
}
return MessagesState( return MessagesState(
roomId = room.roomId, roomId = room.roomId,
roomName = roomName, roomName = roomName,
@ -213,9 +229,8 @@ class MessagesPresenter @AssistedInject constructor(
inviteProgress = inviteProgress.value, inviteProgress = inviteProgress.value,
enableTextFormatting = enableTextFormatting, enableTextFormatting = enableTextFormatting,
enableVoiceMessages = enableVoiceMessages, enableVoiceMessages = enableVoiceMessages,
enableInRoomCalls = enableInRoomCalls,
appName = buildMeta.applicationName, appName = buildMeta.applicationName,
isCallOngoing = roomInfo?.hasRoomCall ?: false, callState = callState,
eventSink = { handleEvents(it) } eventSink = { handleEvents(it) }
) )
} }

View file

@ -51,8 +51,13 @@ data class MessagesState(
val showReinvitePrompt: Boolean, val showReinvitePrompt: Boolean,
val enableTextFormatting: Boolean, val enableTextFormatting: Boolean,
val enableVoiceMessages: Boolean, val enableVoiceMessages: Boolean,
val enableInRoomCalls: Boolean, val callState: RoomCallState,
val isCallOngoing: Boolean,
val appName: String, val appName: String,
val eventSink: (MessagesEvents) -> Unit val eventSink: (MessagesEvents) -> Unit
) )
enum class RoomCallState {
ENABLED,
ONGOING,
DISABLED
}

View file

@ -66,7 +66,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
), ),
), ),
aMessagesState().copy( aMessagesState().copy(
isCallOngoing = true, callState = RoomCallState.ONGOING,
), ),
aMessagesState().copy( aMessagesState().copy(
enableVoiceMessages = true, enableVoiceMessages = true,
@ -75,6 +75,9 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
showSendFailureDialog = true showSendFailureDialog = true
), ),
), ),
aMessagesState().copy(
callState = RoomCallState.DISABLED,
),
) )
} }
@ -117,8 +120,7 @@ fun aMessagesState() = MessagesState(
showReinvitePrompt = false, showReinvitePrompt = false,
enableTextFormatting = true, enableTextFormatting = true,
enableVoiceMessages = true, enableVoiceMessages = true,
enableInRoomCalls = true, callState = RoomCallState.ENABLED,
isCallOngoing = false,
appName = "Element", appName = "Element",
eventSink = {} eventSink = {}
) )

View file

@ -191,10 +191,9 @@ fun MessagesView(
MessagesViewTopBar( MessagesViewTopBar(
roomName = state.roomName.dataOrNull(), roomName = state.roomName.dataOrNull(),
roomAvatar = state.roomAvatar.dataOrNull(), roomAvatar = state.roomAvatar.dataOrNull(),
inRoomCallsEnabled = state.enableInRoomCalls, callState = state.callState,
onBackPressed = onBackPressed, onBackPressed = onBackPressed,
onRoomDetailsClicked = onRoomDetailsClicked, onRoomDetailsClicked = onRoomDetailsClicked,
isCallOngoing = state.isCallOngoing,
onJoinCallClicked = onJoinCallClicked, onJoinCallClicked = onJoinCallClicked,
) )
} }
@ -449,8 +448,7 @@ private fun MessagesViewComposerBottomSheetContents(
private fun MessagesViewTopBar( private fun MessagesViewTopBar(
roomName: String?, roomName: String?,
roomAvatar: AvatarData?, roomAvatar: AvatarData?,
inRoomCallsEnabled: Boolean, callState: RoomCallState,
isCallOngoing: Boolean,
onRoomDetailsClicked: () -> Unit, onRoomDetailsClicked: () -> Unit,
onJoinCallClicked: () -> Unit, onJoinCallClicked: () -> Unit,
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
@ -477,13 +475,11 @@ private fun MessagesViewTopBar(
} }
}, },
actions = { actions = {
if (inRoomCallsEnabled) { if (callState == RoomCallState.ONGOING) {
if (isCallOngoing) { JoinCallMenuItem(onJoinCallClicked = onJoinCallClicked)
JoinCallMenuItem(onJoinCallClicked = onJoinCallClicked) } else {
} else { IconButton(onClick = onJoinCallClicked, enabled = callState != RoomCallState.DISABLED) {
IconButton(onClick = onJoinCallClicked) { Icon(CompoundIcons.VideoCall, contentDescription = stringResource(CommonStrings.a11y_start_call))
Icon(CompoundIcons.VideoCall, contentDescription = stringResource(CommonStrings.a11y_start_call))
}
} }
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))

View file

@ -99,6 +99,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@Suppress("LargeClass")
class MessagesPresenterTest { class MessagesPresenterTest {
@get:Rule @get:Rule
@ -126,6 +127,21 @@ class MessagesPresenterTest {
} }
} }
@Test
fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest {
val room = FakeMatrixRoom().apply {
givenCanUserJoinCall(Result.success(false))
givenRoomInfo(aRoomInfo(hasRoomCall = true))
}
val presenter = createMessagesPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = consumeItemsUntilTimeout().last()
assertThat(initialState.callState).isEqualTo(RoomCallState.DISABLED)
}
}
@Test @Test
fun `present - handle toggling a reaction`() = runTest { fun `present - handle toggling a reaction`() = runTest {
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
@ -656,6 +672,7 @@ class MessagesPresenterTest {
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
currentSessionIdHolder: CurrentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
): MessagesPresenter { ): MessagesPresenter {
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom) val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
@ -724,6 +741,7 @@ class MessagesPresenterTest {
featureFlagsService = FakeFeatureFlagService(), featureFlagsService = FakeFeatureFlagService(),
buildMeta = aBuildMeta(), buildMeta = aBuildMeta(),
dispatchers = coroutineDispatchers, dispatchers = coroutineDispatchers,
currentSessionIdHolder = currentSessionIdHolder,
) )
} }
} }

View file

@ -132,6 +132,8 @@ interface MatrixRoom : Closeable {
suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean> suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean>
suspend fun canUserJoinCall(userId: UserId): Result<Boolean>
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>
suspend fun removeAvatar(): Result<Unit> suspend fun removeAvatar(): Result<Unit>

View file

@ -77,6 +77,7 @@ import org.matrix.rustcomponents.sdk.BackupState
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.NotificationProcessSetup
import org.matrix.rustcomponents.sdk.PowerLevels
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.TaskHandle
@ -275,6 +276,7 @@ class RustMatrixClient constructor(
}, },
invite = createRoomParams.invite?.map { it.value }, invite = createRoomParams.invite?.map { it.value },
avatar = createRoomParams.avatar, avatar = createRoomParams.avatar,
powerLevelContentOverride = defaultRoomCreationPowerLevels,
) )
val roomId = RoomId(client.createRoom(rustParams)) val roomId = RoomId(client.createRoom(rustParams))
@ -297,7 +299,7 @@ class RustMatrixClient constructor(
isDirect = true, isDirect = true,
visibility = RoomVisibility.PRIVATE, visibility = RoomVisibility.PRIVATE,
preset = RoomPreset.TRUSTED_PRIVATE_CHAT, preset = RoomPreset.TRUSTED_PRIVATE_CHAT,
invite = listOf(userId) invite = listOf(userId),
) )
return createRoom(createRoomParams) return createRoom(createRoomParams)
} }
@ -482,3 +484,18 @@ class RustMatrixClient constructor(
} }
} }
private val defaultRoomCreationPowerLevels = PowerLevels(
usersDefault = null,
eventsDefault = null,
stateDefault = null,
ban = null,
kick = null,
redact = null,
invite = null,
notifications = null,
users = mapOf(),
events = mapOf(
"m.call.member" to 0,
"org.matrix.msc3401.call.member" to 0,
)
)

View file

@ -364,6 +364,12 @@ class RustMatrixRoom(
} }
} }
override suspend fun canUserJoinCall(userId: UserId): Result<Boolean> {
return runCatching {
innerRoom.canUserSendState(userId.value, StateEventType.ROOM_MEMBER_EVENT.map())
}
}
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler> { override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler> {
return sendAttachment(listOf(file, thumbnailFile)) { return sendAttachment(listOf(file, thumbnailFile)) {
innerTimeline.sendImage(file.path, thumbnailFile.path, imageInfo.map(), progressCallback?.toProgressWatcher()) innerTimeline.sendImage(file.path, thumbnailFile.path, imageInfo.map(), progressCallback?.toProgressWatcher())

View file

@ -111,6 +111,7 @@ class FakeMatrixRoom(
private var generateWidgetWebViewUrlResult = Result.success("https://call.element.io") private var generateWidgetWebViewUrlResult = Result.success("https://call.element.io")
private var getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeWidgetDriver()) private var getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeWidgetDriver())
private var canUserTriggerRoomNotificationResult: Result<Boolean> = Result.success(true) private var canUserTriggerRoomNotificationResult: Result<Boolean> = Result.success(true)
private var canUserJoinCallResult: Result<Boolean> = Result.success(true)
var sendMessageMentions = emptyList<Mention>() var sendMessageMentions = emptyList<Mention>()
val editMessageCalls = mutableListOf<Pair<String, String?>>() val editMessageCalls = mutableListOf<Pair<String, String?>>()
@ -292,6 +293,10 @@ class FakeMatrixRoom(
return canUserTriggerRoomNotificationResult return canUserTriggerRoomNotificationResult
} }
override suspend fun canUserJoinCall(userId: UserId): Result<Boolean> {
return canUserJoinCallResult
}
override suspend fun sendImage( override suspend fun sendImage(
file: File, file: File,
thumbnailFile: File, thumbnailFile: File,
@ -474,6 +479,10 @@ class FakeMatrixRoom(
canUserTriggerRoomNotificationResult = result canUserTriggerRoomNotificationResult = result
} }
fun givenCanUserJoinCall(result: Result<Boolean>) {
canUserJoinCallResult = result
}
fun givenIgnoreResult(result: Result<Unit>) { fun givenIgnoreResult(result: Result<Unit>) {
ignoreResult = result ignoreResult = result
} }

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6cabc1dec0b89061db2b9f5301371b839a458e7e5bb6c8bc5b4d2fd4fcc3a179
size 54275

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:da867f9dd0e7d21419ce52e2e0eabfc26583e9dfb1523613a932394c307b3e28
size 52621