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:
parent
b94ee9fb0a
commit
0a91184e4a
12 changed files with 98 additions and 21 deletions
|
|
@ -0,0 +1 @@
|
||||||
|
Set a default power level to join calls. Also, create new rooms taking this power level into account.
|
||||||
|
|
@ -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) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6cabc1dec0b89061db2b9f5301371b839a458e7e5bb6c8bc5b4d2fd4fcc3a179
|
||||||
|
size 54275
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:da867f9dd0e7d21419ce52e2e0eabfc26583e9dfb1523613a932394c307b3e28
|
||||||
|
size 52621
|
||||||
Loading…
Add table
Add a link
Reference in a new issue