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.MatrixRoomMembersState
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.AttachmentThumbnailType
import io.element.android.libraries.matrix.ui.room.canRedactAsState
@@ -108,6 +109,7 @@ class MessagesPresenter @AssistedInject constructor(
private val featureFlagsService: FeatureFlagService,
@Assisted private val navigator: MessagesNavigator,
private val buildMeta: BuildMeta,
private val currentSessionIdHolder: CurrentSessionIdHolder,
) : Presenter<MessagesState> {
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
@@ -144,6 +146,16 @@ class MessagesPresenter @AssistedInject constructor(
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) }
var showReinvitePrompt by remember { mutableStateOf(false) }
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) {
@@ -162,8 +174,6 @@ class MessagesPresenter @AssistedInject constructor(
val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
var enableVoiceMessages by remember { mutableStateOf(false) }
// TODO add min power level to use this feature in the future?
val enableInRoomCalls = true
LaunchedEffect(featureFlagsService) {
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(
roomId = room.roomId,
roomName = roomName,
@@ -213,9 +229,8 @@ class MessagesPresenter @AssistedInject constructor(
inviteProgress = inviteProgress.value,
enableTextFormatting = enableTextFormatting,
enableVoiceMessages = enableVoiceMessages,
enableInRoomCalls = enableInRoomCalls,
appName = buildMeta.applicationName,
isCallOngoing = roomInfo?.hasRoomCall ?: false,
callState = callState,
eventSink = { handleEvents(it) }
)
}

View File

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

View File

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

View File

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

View File

@@ -99,6 +99,7 @@ import org.junit.Rule
import org.junit.Test
import kotlin.time.Duration.Companion.milliseconds
@Suppress("LargeClass")
class MessagesPresenterTest {
@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
fun `present - handle toggling a reaction`() = runTest {
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
@@ -656,6 +672,7 @@ class MessagesPresenterTest {
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
currentSessionIdHolder: CurrentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
): MessagesPresenter {
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
@@ -724,6 +741,7 @@ class MessagesPresenterTest {
featureFlagsService = FakeFeatureFlagService(),
buildMeta = aBuildMeta(),
dispatchers = coroutineDispatchers,
currentSessionIdHolder = currentSessionIdHolder,
)
}
}

View File

@@ -132,6 +132,8 @@ interface MatrixRoom : Closeable {
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 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.ClientDelegate
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
import org.matrix.rustcomponents.sdk.PowerLevels
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.TaskHandle
@@ -275,6 +276,7 @@ class RustMatrixClient constructor(
},
invite = createRoomParams.invite?.map { it.value },
avatar = createRoomParams.avatar,
powerLevelContentOverride = defaultRoomCreationPowerLevels,
)
val roomId = RoomId(client.createRoom(rustParams))
@@ -297,7 +299,7 @@ class RustMatrixClient constructor(
isDirect = true,
visibility = RoomVisibility.PRIVATE,
preset = RoomPreset.TRUSTED_PRIVATE_CHAT,
invite = listOf(userId)
invite = listOf(userId),
)
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> {
return sendAttachment(listOf(file, thumbnailFile)) {
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 getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeWidgetDriver())
private var canUserTriggerRoomNotificationResult: Result<Boolean> = Result.success(true)
private var canUserJoinCallResult: Result<Boolean> = Result.success(true)
var sendMessageMentions = emptyList<Mention>()
val editMessageCalls = mutableListOf<Pair<String, String?>>()
@@ -292,6 +293,10 @@ class FakeMatrixRoom(
return canUserTriggerRoomNotificationResult
}
override suspend fun canUserJoinCall(userId: UserId): Result<Boolean> {
return canUserJoinCallResult
}
override suspend fun sendImage(
file: File,
thumbnailFile: File,
@@ -474,6 +479,10 @@ class FakeMatrixRoom(
canUserTriggerRoomNotificationResult = result
}
fun givenCanUserJoinCall(result: Result<Boolean>) {
canUserJoinCallResult = result
}
fun givenIgnoreResult(result: Result<Unit>) {
ignoreResult = result
}