From add737b64654a0c55b4ccba6a76f8b58a8e2fafc Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Feb 2026 17:24:20 +0100 Subject: [PATCH] Rename ShareLocationEvents -> ShareLocationEvent --- ...ocationEvents.kt => ShareLocationEvent.kt} | 18 +-- .../impl/share/ShareLocationPresenter.kt | 21 +-- .../location/impl/share/ShareLocationView.kt | 129 +++++++++++------- .../impl/share/ShareLocationPresenterTest.kt | 36 ++--- 4 files changed, 122 insertions(+), 82 deletions(-) rename features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/{ShareLocationEvents.kt => ShareLocationEvent.kt} (56%) diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvents.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvent.kt similarity index 56% rename from features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvents.kt rename to features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvent.kt index 93b75d8e38..b5d45b8147 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvents.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvent.kt @@ -10,11 +10,11 @@ package io.element.android.features.location.impl.share import io.element.android.features.location.api.Location -sealed interface ShareLocationEvents { - data class ShareLocation( +sealed interface ShareLocationEvent { + data class ShareStaticLocation( val cameraPosition: CameraPosition, val location: Location?, - ) : ShareLocationEvents { + ) : ShareLocationEvent { data class CameraPosition( val lat: Double, val lon: Double, @@ -22,9 +22,11 @@ sealed interface ShareLocationEvents { ) } - data object SwitchToMyLocationMode : ShareLocationEvents - data object SwitchToPinLocationMode : ShareLocationEvents - data object DismissDialog : ShareLocationEvents - data object RequestPermissions : ShareLocationEvents - data object OpenAppSettings : ShareLocationEvents + data object SelectLiveLocationDuration: ShareLocationEvent + + data object SwitchToMyLocationMode : ShareLocationEvent + data object SwitchToPinLocationMode : ShareLocationEvent + data object DismissDialog : ShareLocationEvent + data object RequestPermissions : ShareLocationEvent + data object OpenAppSettings : ShareLocationEvent } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt index 5b2b7ebc25..7f9f9a9874 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt @@ -79,23 +79,24 @@ class ShareLocationPresenter( } } - fun handleEvent(event: ShareLocationEvents) { + fun handleEvent(event: ShareLocationEvent) { when (event) { - is ShareLocationEvents.ShareLocation -> scope.launch { + is ShareLocationEvent.ShareStaticLocation -> scope.launch { shareLocation(event, mode) } - ShareLocationEvents.SwitchToMyLocationMode -> when { + ShareLocationEvent.SwitchToMyLocationMode -> when { permissionsState.isAnyGranted -> mode = ShareLocationState.Mode.SenderLocation permissionsState.shouldShowRationale -> permissionDialog = ShareLocationState.Dialog.PermissionRationale else -> permissionDialog = ShareLocationState.Dialog.PermissionDenied } - ShareLocationEvents.SwitchToPinLocationMode -> mode = ShareLocationState.Mode.PinLocation - ShareLocationEvents.DismissDialog -> permissionDialog = ShareLocationState.Dialog.None - ShareLocationEvents.OpenAppSettings -> { + ShareLocationEvent.SwitchToPinLocationMode -> mode = ShareLocationState.Mode.PinLocation + ShareLocationEvent.DismissDialog -> permissionDialog = ShareLocationState.Dialog.None + ShareLocationEvent.OpenAppSettings -> { locationActions.openSettings() permissionDialog = ShareLocationState.Dialog.None } - ShareLocationEvents.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions) + ShareLocationEvent.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions) + ShareLocationEvent.SelectLiveLocationDuration -> Unit } } @@ -109,7 +110,7 @@ class ShareLocationPresenter( } private suspend fun shareLocation( - event: ShareLocationEvents.ShareLocation, + event: ShareLocationEvent.ShareStaticLocation, mode: ShareLocationState.Mode, ) { val replyMode = messageComposerContext.composerMode as? MessageComposerMode.Reply @@ -168,8 +169,8 @@ class ShareLocationPresenter( } } -private fun ShareLocationEvents.ShareLocation.toGeoUri(): String = location?.toGeoUri() ?: cameraPosition.toGeoUri() +private fun ShareLocationEvent.ShareStaticLocation.toGeoUri(): String = location?.toGeoUri() ?: cameraPosition.toGeoUri() -private fun ShareLocationEvents.ShareLocation.CameraPosition.toGeoUri(): String = "geo:$lat,$lon" +private fun ShareLocationEvent.ShareStaticLocation.CameraPosition.toGeoUri(): String = "geo:$lat,$lon" private fun generateBody(uri: String): String = "Location was shared at $uri" diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt index 1c9e85c82b..6e83406367 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ListItem import androidx.compose.material3.SheetValue import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState @@ -31,24 +30,29 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.location.api.Location import io.element.android.features.location.api.internal.centerBottomEdge import io.element.android.features.location.api.internal.rememberTileStyleUrl -import io.element.android.features.location.impl.R import io.element.android.features.location.impl.common.MapDefaults import io.element.android.features.location.impl.common.PermissionDeniedDialog import io.element.android.features.location.impl.common.PermissionRationaleDialog import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.maplibre.compose.CameraMode import io.element.android.libraries.maplibre.compose.CameraMoveStartedReason +import io.element.android.libraries.maplibre.compose.CameraPositionState import io.element.android.libraries.maplibre.compose.MapLibreMap import io.element.android.libraries.maplibre.compose.rememberCameraPositionState import io.element.android.libraries.ui.strings.CommonStrings @@ -62,19 +66,19 @@ fun ShareLocationView( modifier: Modifier = Modifier, ) { LaunchedEffect(Unit) { - state.eventSink(ShareLocationEvents.RequestPermissions) + state.eventSink(ShareLocationEvent.RequestPermissions) } when (state.permissionDialog) { ShareLocationState.Dialog.None -> Unit ShareLocationState.Dialog.PermissionDenied -> PermissionDeniedDialog( - onContinue = { state.eventSink(ShareLocationEvents.OpenAppSettings) }, - onDismiss = { state.eventSink(ShareLocationEvents.DismissDialog) }, + onContinue = { state.eventSink(ShareLocationEvent.OpenAppSettings) }, + onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) }, appName = state.appName, ) ShareLocationState.Dialog.PermissionRationale -> PermissionRationaleDialog( - onContinue = { state.eventSink(ShareLocationEvents.RequestPermissions) }, - onDismiss = { state.eventSink(ShareLocationEvents.DismissDialog) }, + onContinue = { state.eventSink(ShareLocationEvent.RequestPermissions) }, + onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) }, appName = state.appName, ) } @@ -99,7 +103,7 @@ fun ShareLocationView( LaunchedEffect(cameraPositionState.isMoving) { if (cameraPositionState.cameraMoveStartedReason == CameraMoveStartedReason.GESTURE) { - state.eventSink(ShareLocationEvents.SwitchToPinLocationMode) + state.eventSink(ShareLocationEvent.SwitchToPinLocationMode) } } @@ -108,48 +112,35 @@ fun ShareLocationView( BottomSheetScaffold( sheetContent = { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(Modifier.height(20.dp)) ListItem( headlineContent = { Text( - stringResource( - when (state.mode) { - ShareLocationState.Mode.PinLocation -> CommonStrings.screen_share_this_location_action - ShareLocationState.Mode.SenderLocation -> CommonStrings.screen_share_my_location_action - } - ) + text = "Sharing options", + style = ElementTheme.typography.fontBodyLgMedium, ) - }, - modifier = Modifier.clickable( - // target is null when the map hasn't loaded (or api key is wrong) so we disable the button - enabled = cameraPositionState.position.target != null - ) { - state.eventSink( - ShareLocationEvents.ShareLocation( - cameraPosition = ShareLocationEvents.ShareLocation.CameraPosition( - lat = cameraPositionState.position.target!!.latitude, - lon = cameraPositionState.position.target!!.longitude, - zoom = cameraPositionState.position.zoom, - ), - location = cameraPositionState.location?.let { - Location( - lat = it.latitude, - lon = it.longitude, - accuracy = it.accuracy, - ) - } - ) - ) - navigateUp() - }, - leadingContent = { - Icon( - resourceId = R.drawable.pin_small, - contentDescription = null, - tint = Color.Unspecified, - ) - }, + } ) + StaticLocationItem(state.mode, cameraPositionState){ + val positionTarget = cameraPositionState.position.target ?: return@StaticLocationItem + state.eventSink( + ShareLocationEvent.ShareStaticLocation( + cameraPosition = ShareLocationEvent.ShareStaticLocation.CameraPosition( + lat = positionTarget.latitude, + lon = positionTarget.longitude, + zoom = cameraPositionState.position.zoom, + ), + location = cameraPositionState.location?.let { + Location( + lat = it.latitude, + lon = it.longitude, + accuracy = it.accuracy, + ) + } + ) + ) + navigateUp() + } Spacer(modifier = Modifier.height(16.dp + navBarPadding)) }, modifier = modifier, @@ -191,7 +182,7 @@ fun ShareLocationView( ) LocationFloatingActionButton( isMapCenteredOnUser = state.mode == ShareLocationState.Mode.SenderLocation, - onClick = { state.eventSink(ShareLocationEvents.SwitchToMyLocationMode) }, + onClick = { state.eventSink(ShareLocationEvent.SwitchToMyLocationMode) }, modifier = Modifier .align(Alignment.BottomEnd) .padding(end = 18.dp, bottom = 72.dp + navBarPadding), @@ -200,6 +191,52 @@ fun ShareLocationView( } } +@Composable +private fun StaticLocationItem( + mode: ShareLocationState.Mode, + cameraPositionState: CameraPositionState, + onClick: ()->Unit, +) { + ListItem( + headlineContent = { + Text( + stringResource( + when (mode) { + ShareLocationState.Mode.PinLocation -> CommonStrings.screen_share_this_location_action + ShareLocationState.Mode.SenderLocation -> CommonStrings.screen_share_my_location_action + } + ) + ) + }, + modifier = Modifier.clickable( + // target is null when the map hasn't loaded (or api key is wrong) so we disable the button + enabled = cameraPositionState.position.target != null, + onClick = onClick + ), + leadingContent = ListItemContent.Icon( + iconSource = IconSource.Vector(CompoundIcons.LocationNavigatorCentred()) + ) + ) +} + +@Composable +private fun LiveLocationItem( + onClick: ()->Unit, +) { + ListItem( + headlineContent = { + Text("Share live location") + }, + modifier = Modifier.clickable( + onClick = onClick + ), + leadingContent = ListItemContent.Icon( + iconSource = IconSource.Vector(CompoundIcons.LocationPinSolid()), + tintColor = ElementTheme.colors.iconAccentPrimary, + ) + ) +} + @PreviewsDayNight @Composable internal fun ShareLocationViewPreview( diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt index ead3fb2fe0..3d4a5b519e 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt @@ -83,7 +83,7 @@ class ShareLocationPresenterTest { assertThat(initialState.hasLocationPermission).isTrue() // Swipe the map to switch mode - initialState.eventSink(ShareLocationEvents.SwitchToPinLocationMode) + initialState.eventSink(ShareLocationEvent.SwitchToPinLocationMode) val myLocationState = awaitItem() assertThat(myLocationState.permissionDialog).isEqualTo(ShareLocationState.Dialog.None) assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation) @@ -110,7 +110,7 @@ class ShareLocationPresenterTest { assertThat(initialState.hasLocationPermission).isTrue() // Swipe the map to switch mode - initialState.eventSink(ShareLocationEvents.SwitchToPinLocationMode) + initialState.eventSink(ShareLocationEvent.SwitchToPinLocationMode) val myLocationState = awaitItem() assertThat(myLocationState.permissionDialog).isEqualTo(ShareLocationState.Dialog.None) assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation) @@ -137,7 +137,7 @@ class ShareLocationPresenterTest { assertThat(initialState.hasLocationPermission).isFalse() // Click on the button to switch mode - initialState.eventSink(ShareLocationEvents.SwitchToMyLocationMode) + initialState.eventSink(ShareLocationEvent.SwitchToMyLocationMode) val myLocationState = awaitItem() assertThat(myLocationState.permissionDialog).isEqualTo(ShareLocationState.Dialog.PermissionDenied) assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation) @@ -164,7 +164,7 @@ class ShareLocationPresenterTest { assertThat(initialState.hasLocationPermission).isFalse() // Click on the button to switch mode - initialState.eventSink(ShareLocationEvents.SwitchToMyLocationMode) + initialState.eventSink(ShareLocationEvent.SwitchToMyLocationMode) val myLocationState = awaitItem() assertThat(myLocationState.permissionDialog).isEqualTo(ShareLocationState.Dialog.PermissionRationale) assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation) @@ -189,14 +189,14 @@ class ShareLocationPresenterTest { val initialState = awaitItem() // Click on the button to switch mode - initialState.eventSink(ShareLocationEvents.SwitchToMyLocationMode) + initialState.eventSink(ShareLocationEvent.SwitchToMyLocationMode) val myLocationState = awaitItem() assertThat(myLocationState.permissionDialog).isEqualTo(ShareLocationState.Dialog.PermissionRationale) assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation) assertThat(myLocationState.hasLocationPermission).isFalse() // Dismiss the dialog - myLocationState.eventSink(ShareLocationEvents.DismissDialog) + myLocationState.eventSink(ShareLocationEvent.DismissDialog) val dialogDismissedState = awaitItem() assertThat(dialogDismissedState.permissionDialog).isEqualTo(ShareLocationState.Dialog.None) assertThat(dialogDismissedState.mode).isEqualTo(ShareLocationState.Mode.PinLocation) @@ -221,14 +221,14 @@ class ShareLocationPresenterTest { val initialState = awaitItem() // Click on the button to switch mode - initialState.eventSink(ShareLocationEvents.SwitchToMyLocationMode) + initialState.eventSink(ShareLocationEvent.SwitchToMyLocationMode) val myLocationState = awaitItem() assertThat(myLocationState.permissionDialog).isEqualTo(ShareLocationState.Dialog.PermissionRationale) assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation) assertThat(myLocationState.hasLocationPermission).isFalse() // Continue the dialog sends permission request to the permissions presenter - myLocationState.eventSink(ShareLocationEvents.RequestPermissions) + myLocationState.eventSink(ShareLocationEvent.RequestPermissions) assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvents.RequestPermissions) } } @@ -250,14 +250,14 @@ class ShareLocationPresenterTest { val initialState = awaitItem() // Click on the button to switch mode - initialState.eventSink(ShareLocationEvents.SwitchToMyLocationMode) + initialState.eventSink(ShareLocationEvent.SwitchToMyLocationMode) val myLocationState = awaitItem() assertThat(myLocationState.permissionDialog).isEqualTo(ShareLocationState.Dialog.PermissionDenied) assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation) assertThat(myLocationState.hasLocationPermission).isFalse() // Dismiss the dialog - myLocationState.eventSink(ShareLocationEvents.DismissDialog) + myLocationState.eventSink(ShareLocationEvent.DismissDialog) val dialogDismissedState = awaitItem() assertThat(dialogDismissedState.permissionDialog).isEqualTo(ShareLocationState.Dialog.None) assertThat(dialogDismissedState.mode).isEqualTo(ShareLocationState.Mode.PinLocation) @@ -291,8 +291,8 @@ class ShareLocationPresenterTest { // Send location initialState.eventSink( - ShareLocationEvents.ShareLocation( - cameraPosition = ShareLocationEvents.ShareLocation.CameraPosition( + ShareLocationEvent.ShareStaticLocation( + cameraPosition = ShareLocationEvent.ShareStaticLocation.CameraPosition( lat = 0.0, lon = 1.0, zoom = 2.0, @@ -355,8 +355,8 @@ class ShareLocationPresenterTest { // Send location initialState.eventSink( - ShareLocationEvents.ShareLocation( - cameraPosition = ShareLocationEvents.ShareLocation.CameraPosition( + ShareLocationEvent.ShareStaticLocation( + cameraPosition = ShareLocationEvent.ShareStaticLocation.CameraPosition( lat = 0.0, lon = 1.0, zoom = 2.0, @@ -425,8 +425,8 @@ class ShareLocationPresenterTest { // Send location initialState.eventSink( - ShareLocationEvents.ShareLocation( - cameraPosition = ShareLocationEvents.ShareLocation.CameraPosition( + ShareLocationEvent.ShareStaticLocation( + cameraPosition = ShareLocationEvent.ShareStaticLocation.CameraPosition( lat = 0.0, lon = 1.0, zoom = 2.0, @@ -471,11 +471,11 @@ class ShareLocationPresenterTest { // Skip initial state val initialState = awaitItem() - initialState.eventSink(ShareLocationEvents.SwitchToMyLocationMode) + initialState.eventSink(ShareLocationEvent.SwitchToMyLocationMode) val dialogShownState = awaitItem() // Open settings - dialogShownState.eventSink(ShareLocationEvents.OpenAppSettings) + dialogShownState.eventSink(ShareLocationEvent.OpenAppSettings) val settingsOpenedState = awaitItem() assertThat(settingsOpenedState.permissionDialog).isEqualTo(ShareLocationState.Dialog.None)