Merge pull request #6996 from element-hq/feature/fga/location_permissions_rework

change(location): ensure permissions are always requested at least once
This commit is contained in:
ganfra
2026-06-10 09:58:20 +02:00
committed by GitHub
11 changed files with 161 additions and 36 deletions
@@ -14,6 +14,7 @@ import io.element.android.features.location.impl.common.ui.LocationConstraintsDi
sealed interface LocationConstraintsCheck { sealed interface LocationConstraintsCheck {
data object Success : LocationConstraintsCheck data object Success : LocationConstraintsCheck
data object PermissionRationale : LocationConstraintsCheck data object PermissionRationale : LocationConstraintsCheck
data object PermissionShouldBeRequested : LocationConstraintsCheck
data object PermissionDenied : LocationConstraintsCheck data object PermissionDenied : LocationConstraintsCheck
data object LocationServiceDisabled : LocationConstraintsCheck data object LocationServiceDisabled : LocationConstraintsCheck
data object NotEnoughPowerLevel : LocationConstraintsCheck data object NotEnoughPowerLevel : LocationConstraintsCheck
@@ -34,6 +35,7 @@ fun checkLocationConstraints(
} }
} }
permissionsState.shouldShowRationale -> LocationConstraintsCheck.PermissionRationale permissionsState.shouldShowRationale -> LocationConstraintsCheck.PermissionRationale
!permissionsState.permissionsAlreadyRequested -> LocationConstraintsCheck.PermissionShouldBeRequested
else -> LocationConstraintsCheck.PermissionDenied else -> LocationConstraintsCheck.PermissionDenied
} }
} }
@@ -41,6 +43,7 @@ fun checkLocationConstraints(
fun LocationConstraintsCheck.toDialogState(): LocationConstraintsDialogState { fun LocationConstraintsCheck.toDialogState(): LocationConstraintsDialogState {
return when (this) { return when (this) {
LocationConstraintsCheck.Success -> LocationConstraintsDialogState.None LocationConstraintsCheck.Success -> LocationConstraintsDialogState.None
LocationConstraintsCheck.PermissionShouldBeRequested -> LocationConstraintsDialogState.None
LocationConstraintsCheck.PermissionRationale -> LocationConstraintsDialogState.PermissionRationale LocationConstraintsCheck.PermissionRationale -> LocationConstraintsDialogState.PermissionRationale
LocationConstraintsCheck.PermissionDenied -> LocationConstraintsDialogState.PermissionDenied LocationConstraintsCheck.PermissionDenied -> LocationConstraintsDialogState.PermissionDenied
LocationConstraintsCheck.LocationServiceDisabled -> LocationConstraintsDialogState.LocationServiceDisabled LocationConstraintsCheck.LocationServiceDisabled -> LocationConstraintsDialogState.LocationServiceDisabled
@@ -9,6 +9,10 @@
package io.element.android.features.location.impl.common.permissions package io.element.android.features.location.impl.common.permissions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
@@ -32,11 +36,16 @@ class DefaultPermissionsPresenter(
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
override fun present(): PermissionsState { override fun present(): PermissionsState {
val multiplePermissionsState = rememberMultiplePermissionsState(permissions = permissions) var permissionsRequested by remember { mutableStateOf(false) }
val multiplePermissionsState = rememberMultiplePermissionsState(permissions = permissions) {
permissionsRequested = true
}
fun handleEvent(event: PermissionsEvents) { fun handleEvent(event: PermissionsEvents) {
when (event) { when (event) {
PermissionsEvents.RequestPermissions -> multiplePermissionsState.launchMultiplePermissionRequest() PermissionsEvents.RequestPermissions -> {
multiplePermissionsState.launchMultiplePermissionRequest()
}
} }
} }
@@ -47,6 +56,7 @@ class DefaultPermissionsPresenter(
else -> PermissionsState.Permissions.NoneGranted else -> PermissionsState.Permissions.NoneGranted
}, },
shouldShowRationale = multiplePermissionsState.shouldShowRationale, shouldShowRationale = multiplePermissionsState.shouldShowRationale,
permissionsAlreadyRequested = permissionsRequested,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
@@ -11,6 +11,7 @@ package io.element.android.features.location.impl.common.permissions
data class PermissionsState( data class PermissionsState(
val permissions: Permissions, val permissions: Permissions,
val shouldShowRationale: Boolean, val shouldShowRationale: Boolean,
val permissionsAlreadyRequested: Boolean,
val eventSink: (PermissionsEvents) -> Unit, val eventSink: (PermissionsEvents) -> Unit,
) { ) {
sealed interface Permissions { sealed interface Permissions {
@@ -88,6 +88,8 @@ class ShareLocationPresenter(
var dialogState: ShareLocationState.Dialog by remember { var dialogState: ShareLocationState.Dialog by remember {
mutableStateOf(ShareLocationState.Dialog.None) mutableStateOf(ShareLocationState.Dialog.None)
} }
// true when trying to initiate the live location share
var pendingLiveLocationShare by remember { mutableStateOf(false) }
val startLiveLocationAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) } val startLiveLocationAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val currentUser by client.userProfile.collectAsState() val currentUser by client.userProfile.collectAsState()
val customMapStyleUrl by produceState(AsyncData.Loading()) { val customMapStyleUrl by produceState(AsyncData.Loading()) {
@@ -100,34 +102,55 @@ class ShareLocationPresenter(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
fun checkLocationConstraints() { fun checkLocationConstraints() {
// No need to check SendLiveLocationPermissions here val locationConstraints = checkLocationConstraints(
val locationConstraints = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) permissionsState = permissionsState,
locationActions = locationActions,
// No need to check SendLiveLocationPermissions here
sendLiveLocationPermissions = SendLiveLocationPermissions.GRANTED
)
if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) {
permissionsState.eventSink(PermissionsEvents.RequestPermissions)
}
trackUserPosition = locationConstraints is LocationConstraintsCheck.Success trackUserPosition = locationConstraints is LocationConstraintsCheck.Success
dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState()) dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState())
} }
suspend fun computeLiveLocationDialogState(): ShareLocationState.Dialog { suspend fun checkLiveLocationConstraints() {
val hasAcceptedDisclaimer = liveLocationStore.hasAcceptedLiveLocationDisclaimer() val locationConstraints = checkLocationConstraints(
val constraintsResult = checkLocationConstraints(permissionsState, locationActions, sendLiveLocationPermissions) permissionsState = permissionsState,
return when { locationActions = locationActions,
!hasAcceptedDisclaimer -> { sendLiveLocationPermissions = sendLiveLocationPermissions,
ShareLocationState.Dialog.LiveLocationDisclaimer )
} when (locationConstraints) {
constraintsResult is LocationConstraintsCheck.Success -> { LocationConstraintsCheck.Success -> {
val durations = LIVE_LOCATION_DURATIONS.map { val hasAcceptedDisclaimer = liveLocationStore.hasAcceptedLiveLocationDisclaimer()
LiveLocationDuration(duration = it, formatted = durationFormatter.format(it)) dialogState = if (!hasAcceptedDisclaimer) {
ShareLocationState.Dialog.LiveLocationDisclaimer
} else {
val durations = LIVE_LOCATION_DURATIONS.map {
LiveLocationDuration(duration = it, formatted = durationFormatter.format(it))
}
ShareLocationState.Dialog.LiveLocationDurations(durations.toImmutableList())
} }
ShareLocationState.Dialog.LiveLocationDurations(durations.toImmutableList())
} }
else -> { else -> {
ShareLocationState.Dialog.Constraints(constraintsResult.toDialogState()) if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) {
permissionsState.eventSink(PermissionsEvents.RequestPermissions)
}
dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState())
} }
} }
} }
val userLocationState = userLocationStateFactory.create(permissionsState.isAnyGranted) val userLocationState = userLocationStateFactory.create(permissionsState.isAnyGranted)
LaunchedEffect(permissionsState.permissions) { checkLocationConstraints() } LaunchedEffect(permissionsState) {
if (pendingLiveLocationShare) {
checkLiveLocationConstraints()
} else {
checkLocationConstraints()
}
}
fun handleEvent(event: ShareLocationEvent) { fun handleEvent(event: ShareLocationEvent) {
when (event) { when (event) {
@@ -136,7 +159,10 @@ class ShareLocationPresenter(
} }
ShareLocationEvent.StartTrackingUserLocation -> checkLocationConstraints() ShareLocationEvent.StartTrackingUserLocation -> checkLocationConstraints()
ShareLocationEvent.StopTrackingUserLocation -> trackUserPosition = false ShareLocationEvent.StopTrackingUserLocation -> trackUserPosition = false
ShareLocationEvent.DismissDialog -> dialogState = ShareLocationState.Dialog.None ShareLocationEvent.DismissDialog -> {
pendingLiveLocationShare = false
dialogState = ShareLocationState.Dialog.None
}
ShareLocationEvent.OpenAppSettings -> { ShareLocationEvent.OpenAppSettings -> {
locationActions.openAppSettings() locationActions.openAppSettings()
dialogState = ShareLocationState.Dialog.None dialogState = ShareLocationState.Dialog.None
@@ -146,15 +172,17 @@ class ShareLocationPresenter(
dialogState = ShareLocationState.Dialog.None dialogState = ShareLocationState.Dialog.None
} }
ShareLocationEvent.InitiateLiveLocationShare -> scope.launch { ShareLocationEvent.InitiateLiveLocationShare -> scope.launch {
dialogState = computeLiveLocationDialogState() pendingLiveLocationShare = true
checkLiveLocationConstraints()
} }
ShareLocationEvent.AcceptLiveLocationDisclaimer -> scope.launch { ShareLocationEvent.AcceptLiveLocationDisclaimer -> scope.launch {
liveLocationStore.setAcceptedLiveLocationDisclaimer() liveLocationStore.setAcceptedLiveLocationDisclaimer()
.onSuccess { .onSuccess {
dialogState = computeLiveLocationDialogState() checkLiveLocationConstraints()
} }
} }
is ShareLocationEvent.StartLiveLocationShare -> scope.launch { is ShareLocationEvent.StartLiveLocationShare -> scope.launch {
pendingLiveLocationShare = false
dialogState = ShareLocationState.Dialog.None dialogState = ShareLocationState.Dialog.None
startLiveLocationAction.runUpdatingState { startLiveLocationAction.runUpdatingState {
liveLocationShareManager.startShare( liveLocationShareManager.startShare(
@@ -90,10 +90,22 @@ import kotlinx.coroutines.launch
value = AsyncData.Success(client.getMapStyleUrl().getOrNull()) value = AsyncData.Success(client.getMapStyleUrl().getOrNull())
} }
LaunchedEffect(permissionsState.permissions) { fun checkLocationConstraints() {
if (permissionsState.isAnyGranted) { val locationConstraints = checkLocationConstraints(
dialogState = LocationConstraintsDialogState.None permissionsState = permissionsState,
locationActions = locationActions,
// No need to check SendLiveLocationPermissions here
sendLiveLocationPermissions = SendLiveLocationPermissions.GRANTED
)
if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) {
permissionsState.eventSink(PermissionsEvents.RequestPermissions)
} }
isTrackMyLocation = locationConstraints is LocationConstraintsCheck.Success
dialogState = locationConstraints.toDialogState()
}
LaunchedEffect(permissionsState) {
checkLocationConstraints()
} }
fun handleEvent(event: ShowLocationEvent) { fun handleEvent(event: ShowLocationEvent) {
@@ -103,9 +115,7 @@ import kotlinx.coroutines.launch
} }
is ShowLocationEvent.TrackMyLocation -> { is ShowLocationEvent.TrackMyLocation -> {
if (event.enabled) { if (event.enabled) {
val locationConstraints = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) checkLocationConstraints()
isTrackMyLocation = locationConstraints is LocationConstraintsCheck.Success
dialogState = locationConstraints.toDialogState()
} else { } else {
isTrackMyLocation = false isTrackMyLocation = false
} }
@@ -13,10 +13,12 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS
fun aPermissionsState( fun aPermissionsState(
permissions: PermissionsState.Permissions = PermissionsState.Permissions.NoneGranted, permissions: PermissionsState.Permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale: Boolean = false, shouldShowRationale: Boolean = false,
permissionsRequested: Boolean = false,
): PermissionsState { ): PermissionsState {
return PermissionsState( return PermissionsState(
permissions = permissions, permissions = permissions,
shouldShowRationale = shouldShowRationale, shouldShowRationale = shouldShowRationale,
permissionsAlreadyRequested = permissionsRequested,
eventSink = {}, eventSink = {},
) )
} }
@@ -64,10 +64,25 @@ class LocationConstraintsCheckTest {
} }
@Test @Test
fun `checkLocationConstraints returns PermissionDenied when permissions denied without rationale`() { fun `checkLocationConstraints returns PermissionShouldBeRequested when permissions not yet requested`() {
val permissionsState = aPermissionsState( val permissionsState = aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
permissionsRequested = false,
)
val locationActions = FakeLocationActions(isLocationEnabled = true)
val result = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED)
assertThat(result).isEqualTo(LocationConstraintsCheck.PermissionShouldBeRequested)
}
@Test
fun `checkLocationConstraints returns PermissionDenied when permissions already requested and denied without rationale`() {
val permissionsState = aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
permissionsRequested = true,
) )
val locationActions = FakeLocationActions(isLocationEnabled = true) val locationActions = FakeLocationActions(isLocationEnabled = true)
@@ -20,6 +20,7 @@ class FakePermissionsPresenter : PermissionsPresenter {
private var state = PermissionsState( private var state = PermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
permissionsAlreadyRequested = false,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
set(value) { set(value) {
@@ -160,6 +160,24 @@ class ShareLocationPresenterTest {
} }
} }
@Test
fun `initial state with permissions not yet requested triggers permission request`() = runTest {
val shareLocationPresenter = createShareLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
permissionsRequested = false,
)
)
shareLocationPresenter.test {
skipItems(2)
cancelAndIgnoreRemainingEvents()
assertThat(fakePermissionsPresenter.events).contains(PermissionsEvents.RequestPermissions)
}
}
@Test @Test
fun `initial state with permissions denied`() = runTest { fun `initial state with permissions denied`() = runTest {
val shareLocationPresenter = createShareLocationPresenter() val shareLocationPresenter = createShareLocationPresenter()
@@ -167,6 +185,7 @@ class ShareLocationPresenterTest {
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
permissionsRequested = true,
) )
) )
@@ -291,6 +310,7 @@ class ShareLocationPresenterTest {
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
permissionsRequested = true,
) )
) )
@@ -359,7 +379,12 @@ class ShareLocationPresenterTest {
@Test @Test
fun `ShowLiveLocationDurationPicker shows disclaimer when acceptance is missing`() = runTest { fun `ShowLiveLocationDurationPicker shows disclaimer when acceptance is missing`() = runTest {
val presenter = createShareLocationPresenter() val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
roomPermissions = grantedSendLiveLocationPermissions()
)
)
val presenter = createShareLocationPresenter(joinedRoom = room)
fakePermissionsPresenter.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted, permissions = PermissionsState.Permissions.AllGranted,
@@ -462,7 +487,12 @@ class ShareLocationPresenterTest {
@Test @Test
fun `ShowLiveLocationDurationPicker uses the active session disclaimer state`() = runTest { fun `ShowLiveLocationDurationPicker uses the active session disclaimer state`() = runTest {
val joinedRoom = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = SessionId("@alice:server"))) val joinedRoom = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
sessionId = SessionId("@alice:server"),
roomPermissions = grantedSendLiveLocationPermissions()
),
)
createLiveLocationStore(sessionId = SessionId("@bob:server")) createLiveLocationStore(sessionId = SessionId("@bob:server"))
.setAcceptedLiveLocationDisclaimer() .setAcceptedLiveLocationDisclaimer()
.getOrThrow() .getOrThrow()
@@ -505,12 +535,13 @@ class ShareLocationPresenterTest {
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
permissionsRequested = true,
) )
) )
shareLocationPresenter.test { shareLocationPresenter.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
// Dismiss initial dialog // Dismiss initial dialog to allow re-triggering it
initialState.eventSink(ShareLocationEvent.DismissDialog) initialState.eventSink(ShareLocationEvent.DismissDialog)
val dismissedState = awaitItem() val dismissedState = awaitItem()
@@ -37,7 +37,6 @@ import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@@ -103,8 +102,8 @@ class ShowLocationPresenterTest {
val presenter = createShowLocationPresenter() val presenter = createShowLocationPresenter()
presenter.test { presenter.test {
val initialState = awaitItem() assertThat(awaitItem().isTrackMyLocation).isFalse()
assertThat(initialState.isTrackMyLocation).isFalse() assertThat(awaitItem().isTrackMyLocation).isTrue()
} }
} }
@@ -129,15 +128,12 @@ class ShowLocationPresenterTest {
) )
) )
presenter.test { presenter.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isTrackMyLocation).isFalse() assertThat(initialState.isTrackMyLocation).isFalse()
initialState.eventSink(ShowLocationEvent.TrackMyLocation(true)) initialState.eventSink(ShowLocationEvent.TrackMyLocation(true))
val trackMyLocationState = awaitItem() val trackMyLocationState = awaitItem()
delay(1)
assertThat(trackMyLocationState.isTrackMyLocation).isTrue() assertThat(trackMyLocationState.isTrackMyLocation).isTrue()
// Swipe the map to switch mode // Swipe the map to switch mode
@@ -201,12 +197,34 @@ class ShowLocationPresenterTest {
} }
} }
@Test
fun `TrackMyLocation with permissions not yet requested triggers permission request`() = runTest {
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
permissionsRequested = false,
)
)
val presenter = createShowLocationPresenter()
presenter.test {
val initialState = awaitItem()
initialState.eventSink(ShowLocationEvent.TrackMyLocation(true))
assertThat(fakePermissionsPresenter.events).contains(PermissionsEvents.RequestPermissions)
cancelAndIgnoreRemainingEvents()
}
}
@Test @Test
fun `permission denied dialog dismiss`() = runTest { fun `permission denied dialog dismiss`() = runTest {
fakePermissionsPresenter.givenState( fakePermissionsPresenter.givenState(
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
permissionsRequested = true,
) )
) )
@@ -235,6 +253,7 @@ class ShowLocationPresenterTest {
aPermissionsState( aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted, permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false, shouldShowRationale = false,
permissionsRequested = true,
) )
) )
@@ -21,6 +21,7 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
@@ -48,6 +49,8 @@ class RoomMemberModerationViewTest {
onSelectAction = callback onSelectAction = callback
) )
clickOn(R.string.screen_bottom_sheet_manage_room_member_member_user_info) clickOn(R.string.screen_bottom_sheet_manage_room_member_member_user_info)
// Gives time for bottomsheet to hide
mainClock.advanceTimeBy(1_000)
} }
} }
@@ -217,11 +220,13 @@ class RoomMemberModerationViewTest {
private fun AndroidComposeUiTest<ComponentActivity>.setRoomMemberModerationView( private fun AndroidComposeUiTest<ComponentActivity>.setRoomMemberModerationView(
state: InternalRoomMemberModerationState, state: InternalRoomMemberModerationState,
onSelectAction: (ModerationAction, MatrixUser) -> Unit = EnsureNeverCalledWithTwoParams(), onSelectAction: (ModerationAction, MatrixUser) -> Unit = EnsureNeverCalledWithTwoParams(),
onAvatarClick: ((MatrixUser) -> Unit)? = EnsureNeverCalledWithParam(),
) { ) {
setSafeContent { setSafeContent {
RoomMemberModerationView( RoomMemberModerationView(
state = state, state = state,
onSelectAction = onSelectAction, onSelectAction = onSelectAction,
onAvatarClick = onAvatarClick,
) )
} }
} }