mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 03:06:14 +00:00
Merge pull request #6955 from element-hq/feature/fga/remove_location_dot_when_sharing
Change: hide UserLocationPuck when sharing from current device
This commit is contained in:
-42
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import org.maplibre.compose.location.DesiredAccuracy
|
||||
import org.maplibre.compose.location.Location
|
||||
import org.maplibre.compose.location.rememberNullLocationProvider
|
||||
import org.maplibre.spatialk.units.extensions.meters
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class UserLocationState(locationState: State<Location?>) {
|
||||
val location: Location? by locationState
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@Composable
|
||||
fun rememberUserLocationState(hasLocationPermission: Boolean): UserLocationState {
|
||||
val isPreview = LocalInspectionMode.current
|
||||
val locationProvider = if (isPreview || !hasLocationPermission) {
|
||||
rememberNullLocationProvider()
|
||||
} else {
|
||||
rememberPlatformLocationProvider(
|
||||
updateInterval = 5.seconds,
|
||||
desiredAccuracy = DesiredAccuracy.High,
|
||||
minDistance = 5.meters,
|
||||
)
|
||||
}
|
||||
val locationState = locationProvider.location.collectAsState()
|
||||
return remember { UserLocationState(locationState) }
|
||||
}
|
||||
+18
-36
@@ -10,9 +10,8 @@ package io.element.android.features.location.impl.common.ui
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.UserLocationState
|
||||
import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.compose.location.Location
|
||||
import org.maplibre.compose.location.LocationPuck
|
||||
import org.maplibre.compose.location.LocationPuckColors
|
||||
import org.maplibre.compose.location.LocationPuckSizes
|
||||
@@ -20,40 +19,23 @@ import org.maplibre.compose.location.LocationPuckSizes
|
||||
@Composable
|
||||
fun UserLocationPuck(
|
||||
cameraState: CameraState,
|
||||
locationState: UserLocationState,
|
||||
trackUserLocation: Boolean,
|
||||
location: Location?,
|
||||
) {
|
||||
SimpleLocationTrackingEffect(
|
||||
locationState = locationState,
|
||||
enabled = trackUserLocation,
|
||||
) { currentLocation ->
|
||||
val target = currentLocation?.position?.value ?: cameraState.position.target
|
||||
val newPosition = cameraState.position.copy(
|
||||
target = target,
|
||||
// Force pointing to NORTH
|
||||
bearing = 0.0,
|
||||
zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM)
|
||||
LocationPuck(
|
||||
idPrefix = "user-location",
|
||||
location = location,
|
||||
cameraState = cameraState,
|
||||
accuracyThreshold = Float.POSITIVE_INFINITY,
|
||||
showBearingAccuracy = false,
|
||||
showBearing = false,
|
||||
sizes = LocationPuckSizes(
|
||||
dotRadius = 8.dp,
|
||||
dotStrokeWidth = 2.dp,
|
||||
),
|
||||
colors = LocationPuckColors(
|
||||
dotFillColorCurrentLocation = ElementTheme.colors.iconAccentPrimary,
|
||||
dotFillColorOldLocation = ElementTheme.colors.iconAccentTertiary,
|
||||
dotStrokeColor = ElementTheme.colors.bgCanvasDefault,
|
||||
)
|
||||
cameraState.animateTo(newPosition)
|
||||
}
|
||||
val location = locationState.location
|
||||
if (location != null) {
|
||||
LocationPuck(
|
||||
idPrefix = "user-location",
|
||||
location = location,
|
||||
cameraState = cameraState,
|
||||
accuracyThreshold = Float.POSITIVE_INFINITY,
|
||||
showBearingAccuracy = false,
|
||||
showBearing = false,
|
||||
sizes = LocationPuckSizes(
|
||||
dotRadius = 8.dp,
|
||||
dotStrokeWidth = 2.dp,
|
||||
),
|
||||
colors = LocationPuckColors(
|
||||
dotFillColorCurrentLocation = ElementTheme.colors.iconAccentPrimary,
|
||||
dotFillColorOldLocation = ElementTheme.colors.iconAccentTertiary,
|
||||
dotStrokeColor = ElementTheme.colors.bgCanvasDefault,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common.userlocation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import org.maplibre.compose.location.DesiredAccuracy
|
||||
import org.maplibre.compose.location.rememberNullLocationProvider
|
||||
import org.maplibre.spatialk.units.extensions.meters
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultUserLocationStateFactory : UserLocationState.Factory {
|
||||
@Composable
|
||||
override fun create(hasLocationPermission: Boolean): UserLocationState {
|
||||
val locationProvider = if (!hasLocationPermission) {
|
||||
rememberNullLocationProvider()
|
||||
} else {
|
||||
@SuppressLint("MissingPermission")
|
||||
rememberPlatformLocationProvider(
|
||||
updateInterval = 5.seconds,
|
||||
desiredAccuracy = DesiredAccuracy.High,
|
||||
minDistance = 5.meters,
|
||||
)
|
||||
}
|
||||
val location by locationProvider.location.collectAsState()
|
||||
return UserLocationState(location)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common.userlocation
|
||||
|
||||
import io.element.android.features.location.api.Location
|
||||
import org.maplibre.compose.location.PositionWithAccuracy
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
import org.maplibre.spatialk.units.extensions.meters
|
||||
import kotlin.time.TimeSource
|
||||
import org.maplibre.compose.location.Location as MapLibreLocation
|
||||
|
||||
fun Location.asMapLibreLocation(): MapLibreLocation {
|
||||
return MapLibreLocation(
|
||||
position = PositionWithAccuracy(
|
||||
value = Position(latitude = lat, longitude = lon),
|
||||
accuracy = accuracy?.toDouble()?.meters
|
||||
),
|
||||
// Not relevant as not used
|
||||
timestamp = TimeSource.Monotonic.markNow(),
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common
|
||||
package io.element.android.features.location.impl.common.userlocation
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common.userlocation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.maplibre.compose.location.Location
|
||||
|
||||
data class UserLocationState(val location: Location?) {
|
||||
fun interface Factory {
|
||||
@Composable
|
||||
fun create(hasLocationPermission: Boolean): UserLocationState
|
||||
}
|
||||
}
|
||||
+31
-5
@@ -5,15 +5,16 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common.ui
|
||||
package io.element.android.features.location.impl.common.userlocation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import io.element.android.features.location.impl.common.UserLocationState
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.compose.location.Location
|
||||
import kotlin.math.abs
|
||||
|
||||
@@ -22,16 +23,17 @@ import kotlin.math.abs
|
||||
* TODO remove once https://github.com/maplibre/maplibre-compose/issues/808 is fixed
|
||||
*/
|
||||
@Composable
|
||||
internal fun SimpleLocationTrackingEffect(
|
||||
internal fun UserLocationTrackingEffect(
|
||||
locationState: UserLocationState,
|
||||
enabled: Boolean = true,
|
||||
precision: Double = 0.00001,
|
||||
onLocationChange: suspend (Location?) -> Unit,
|
||||
) {
|
||||
val latestOnLocationChange by rememberUpdatedState(onLocationChange)
|
||||
LaunchedEffect(locationState, enabled) {
|
||||
val latestLocationState by rememberUpdatedState(locationState)
|
||||
LaunchedEffect(enabled) {
|
||||
if (!enabled) return@LaunchedEffect
|
||||
val locationStateFlow = snapshotFlow { locationState.location }
|
||||
val locationStateFlow = snapshotFlow { latestLocationState.location }
|
||||
locationStateFlow
|
||||
.distinctUntilChanged { oldLocation, newLocation ->
|
||||
if (oldLocation != null && newLocation != null) {
|
||||
@@ -49,3 +51,27 @@ internal fun SimpleLocationTrackingEffect(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun UserLocationTrackingEffect(
|
||||
cameraState: CameraState,
|
||||
locationState: UserLocationState,
|
||||
enabled: Boolean = true,
|
||||
precision: Double = 0.00001,
|
||||
) {
|
||||
UserLocationTrackingEffect(
|
||||
locationState = locationState,
|
||||
enabled = enabled,
|
||||
precision = precision
|
||||
) { location ->
|
||||
val target = location?.position?.value ?: cameraState.position.target
|
||||
cameraState.animateTo(
|
||||
cameraState.position.copy(
|
||||
target = target,
|
||||
// Force pointing to NORTH
|
||||
bearing = 0.0,
|
||||
zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.ServiceCompat
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.location.impl.common.PlatformLocationProvider
|
||||
import io.element.android.features.location.impl.common.userlocation.PlatformLocationProvider
|
||||
import io.element.android.features.location.impl.di.LocationBindings
|
||||
import io.element.android.features.location.impl.live.notification.LiveLocationSharingNotificationCreator
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
|
||||
+5
-1
@@ -33,6 +33,7 @@ import io.element.android.features.location.impl.common.permissions.PermissionsP
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.features.location.impl.common.sendLiveLocationPermissions
|
||||
import io.element.android.features.location.impl.common.toDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.features.location.impl.live.LiveLocationStore
|
||||
import io.element.android.features.messages.api.MessageComposerContext
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
@@ -70,6 +71,7 @@ class ShareLocationPresenter(
|
||||
private val durationFormatter: DurationFormatter,
|
||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||
private val liveLocationStore: LiveLocationStore,
|
||||
private val userLocationStateFactory: UserLocationState.Factory,
|
||||
) : Presenter<ShareLocationState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
@@ -123,6 +125,8 @@ class ShareLocationPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
val userLocationState = userLocationStateFactory.create(permissionsState.isAnyGranted)
|
||||
|
||||
LaunchedEffect(permissionsState.permissions) { checkLocationConstraints() }
|
||||
|
||||
fun handleEvent(event: ShareLocationEvent) {
|
||||
@@ -171,7 +175,7 @@ class ShareLocationPresenter(
|
||||
currentUser = currentUser,
|
||||
dialogState = dialogState,
|
||||
trackUserLocation = trackUserPosition,
|
||||
hasLocationPermission = permissionsState.isAnyGranted,
|
||||
userLocationState = userLocationState,
|
||||
canShareLiveLocation = timelineMode.canShareLiveLocation(),
|
||||
appName = appName,
|
||||
startLiveLocationAction = startLiveLocationAction.value,
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@
|
||||
package io.element.android.features.location.impl.share
|
||||
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
@@ -19,7 +20,7 @@ data class ShareLocationState(
|
||||
val currentUser: MatrixUser,
|
||||
val dialogState: Dialog,
|
||||
val trackUserLocation: Boolean,
|
||||
val hasLocationPermission: Boolean,
|
||||
val userLocationState: UserLocationState,
|
||||
val appName: String,
|
||||
val canShareLiveLocation: Boolean,
|
||||
val startLiveLocationAction: AsyncAction<Unit>,
|
||||
|
||||
+3
-11
@@ -10,6 +10,7 @@ package io.element.android.features.location.impl.share
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
@@ -26,43 +27,35 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.None,
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = false,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied),
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = false,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale),
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = false,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled),
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.None,
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.None,
|
||||
trackUserPosition = true,
|
||||
hasLocationPermission = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.None,
|
||||
trackUserPosition = true,
|
||||
hasLocationPermission = true,
|
||||
canShareLiveLocation = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.LiveLocationDisclaimer,
|
||||
trackUserPosition = true,
|
||||
hasLocationPermission = true,
|
||||
canShareLiveLocation = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
@@ -74,7 +67,6 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
|
||||
)
|
||||
),
|
||||
trackUserPosition = true,
|
||||
hasLocationPermission = true,
|
||||
canShareLiveLocation = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
@@ -88,7 +80,7 @@ fun aShareLocationState(
|
||||
currentUser: MatrixUser = MatrixUser(UserId("@user:matrix.org")),
|
||||
dialogState: ShareLocationState.Dialog = ShareLocationState.Dialog.None,
|
||||
trackUserPosition: Boolean = false,
|
||||
hasLocationPermission: Boolean = false,
|
||||
userLocationState: UserLocationState = UserLocationState(null),
|
||||
canShareLiveLocation: Boolean = false,
|
||||
appName: String = APP_NAME,
|
||||
startLiveLocationAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
@@ -99,7 +91,7 @@ fun aShareLocationState(
|
||||
currentUser = currentUser,
|
||||
dialogState = dialogState,
|
||||
trackUserLocation = trackUserPosition,
|
||||
hasLocationPermission = hasLocationPermission,
|
||||
userLocationState = userLocationState,
|
||||
canShareLiveLocation = canShareLiveLocation,
|
||||
appName = appName,
|
||||
startLiveLocationAction = startLiveLocationAction,
|
||||
|
||||
+8
-8
@@ -39,12 +39,11 @@ import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
||||
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.UserLocationState
|
||||
import io.element.android.features.location.impl.common.rememberUserLocationState
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialog
|
||||
import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton
|
||||
import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold
|
||||
import io.element.android.features.location.impl.common.ui.UserLocationPuck
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationTrackingEffect
|
||||
import io.element.android.features.location.impl.share.ShareLocationEvent.StartLiveLocationShare
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.LocationPin
|
||||
@@ -109,7 +108,6 @@ fun ShareLocationView(
|
||||
bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded)
|
||||
)
|
||||
val cameraState = rememberCameraState(firstPosition = MapDefaults.defaultCameraPosition)
|
||||
val userLocationState = rememberUserLocationState(state.hasLocationPermission)
|
||||
|
||||
LaunchedEffect(cameraState.isCameraMoving) {
|
||||
if (cameraState.moveReason == CameraMoveReason.GESTURE) {
|
||||
@@ -136,15 +134,18 @@ fun ShareLocationView(
|
||||
BottomSheetContent(
|
||||
cameraState = cameraState,
|
||||
state = state,
|
||||
userLocationState = userLocationState,
|
||||
navigateUp = navigateUp
|
||||
)
|
||||
},
|
||||
mapContent = {
|
||||
UserLocationTrackingEffect(
|
||||
cameraState = cameraState,
|
||||
locationState = state.userLocationState,
|
||||
enabled = state.trackUserLocation,
|
||||
)
|
||||
UserLocationPuck(
|
||||
cameraState = cameraState,
|
||||
locationState = userLocationState,
|
||||
trackUserLocation = state.trackUserLocation
|
||||
location = state.userLocationState.location,
|
||||
)
|
||||
},
|
||||
overlayContent = { sheetPadding ->
|
||||
@@ -215,11 +216,10 @@ private fun StartLiveLocationActionView(
|
||||
private fun BottomSheetContent(
|
||||
cameraState: CameraState,
|
||||
state: ShareLocationState,
|
||||
userLocationState: UserLocationState,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
Spacer(Modifier.height(20.dp))
|
||||
val userLocation = userLocationState.location
|
||||
val userLocation = state.userLocationState.location
|
||||
if (state.trackUserLocation && userLocation != null) {
|
||||
ShareCurrentLocationItem {
|
||||
state.eventSink(
|
||||
|
||||
+54
-39
@@ -10,12 +10,14 @@ package io.element.android.features.location.impl.show
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
@@ -23,6 +25,7 @@ import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.ShowLocationMode
|
||||
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
||||
import io.element.android.features.location.api.live.isCurrentlySharing
|
||||
import io.element.android.features.location.impl.common.LocationConstraintsCheck
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.SendLiveLocationPermissions
|
||||
@@ -33,6 +36,8 @@ import io.element.android.features.location.impl.common.permissions.PermissionsP
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.features.location.impl.common.toDialogState
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.features.location.impl.common.userlocation.asMapLibreLocation
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.mapState
|
||||
@@ -52,8 +57,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AssistedInject
|
||||
class ShowLocationPresenter(
|
||||
@AssistedInject class ShowLocationPresenter(
|
||||
@Assisted private val mode: ShowLocationMode,
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
private val locationActions: LocationActions,
|
||||
@@ -63,9 +67,9 @@ class ShowLocationPresenter(
|
||||
private val client: MatrixClient,
|
||||
private val joinedRoom: JoinedRoom,
|
||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||
private val userLocationStateFactory: UserLocationState.Factory,
|
||||
) : Presenter<ShowLocationState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
@AssistedFactory fun interface Factory {
|
||||
fun create(mode: ShowLocationMode): ShowLocationPresenter
|
||||
}
|
||||
|
||||
@@ -155,55 +159,66 @@ class ShowLocationPresenter(
|
||||
val liveLocationSharesFlow = joinedRoom.subscribeToLiveLocationShares()
|
||||
val membersStateFlow = joinedRoom.membersStateFlow.mapState { it.joinedRoomMembers() }
|
||||
combine(liveLocationSharesFlow, membersStateFlow) { liveShares, members ->
|
||||
liveShares
|
||||
.sortedWith(comparator)
|
||||
.mapNotNull { share ->
|
||||
val lastLocation = share.lastLocation ?: return@mapNotNull null
|
||||
val location = Location.fromGeoUri(lastLocation.geoUri) ?: return@mapNotNull null
|
||||
val member = members.find { it.userId == share.userId }
|
||||
val displayName = member?.getBestName() ?: share.userId.value
|
||||
val avatarUrl = member?.avatarUrl
|
||||
val relativeTime = dateFormatter.format(timestamp = lastLocation.timestamp, mode = DateFormatterMode.Full, useRelative = true)
|
||||
val formattedTimestamp = stringProvider.getString(
|
||||
CommonStrings.screen_static_location_sheet_timestamp_description,
|
||||
relativeTime
|
||||
)
|
||||
LocationShareItem(
|
||||
userId = share.userId,
|
||||
displayName = displayName,
|
||||
avatarData = AvatarData(
|
||||
id = share.userId.value,
|
||||
name = displayName,
|
||||
url = avatarUrl,
|
||||
size = AvatarSize.UserListItem,
|
||||
),
|
||||
formattedTimestamp = formattedTimestamp,
|
||||
location = location,
|
||||
isLive = true,
|
||||
assetType = lastLocation.assetType,
|
||||
isOwnUser = share.userId == joinedRoom.sessionId
|
||||
)
|
||||
}
|
||||
.toImmutableList()
|
||||
liveShares.sortedWith(comparator).mapNotNull { share ->
|
||||
val lastLocation = share.lastLocation ?: return@mapNotNull null
|
||||
val location = Location.fromGeoUri(lastLocation.geoUri) ?: return@mapNotNull null
|
||||
val member = members.find { it.userId == share.userId }
|
||||
val displayName = member?.getBestName() ?: share.userId.value
|
||||
val avatarUrl = member?.avatarUrl
|
||||
val relativeTime = dateFormatter.format(timestamp = lastLocation.timestamp, mode = DateFormatterMode.Full, useRelative = true)
|
||||
val formattedTimestamp = stringProvider.getString(
|
||||
CommonStrings.screen_static_location_sheet_timestamp_description,
|
||||
relativeTime
|
||||
)
|
||||
LocationShareItem(
|
||||
userId = share.userId,
|
||||
displayName = displayName,
|
||||
avatarData = AvatarData(
|
||||
id = share.userId.value,
|
||||
name = displayName,
|
||||
url = avatarUrl,
|
||||
size = AvatarSize.UserListItem,
|
||||
),
|
||||
formattedTimestamp = formattedTimestamp,
|
||||
location = location,
|
||||
isLive = true,
|
||||
assetType = lastLocation.assetType,
|
||||
isOwnUser = share.userId == joinedRoom.sessionId
|
||||
)
|
||||
}.toImmutableList()
|
||||
}.collect { value = it }
|
||||
}.value
|
||||
}
|
||||
}
|
||||
|
||||
val focusedLocation = when (mode) {
|
||||
is ShowLocationMode.Static -> locationShares.firstOrNull()
|
||||
is ShowLocationMode.Live -> locationShares.firstOrNull { it.userId == mode.senderId }
|
||||
val updatedLocationShares by rememberUpdatedState(locationShares)
|
||||
val focusedLocation by remember {
|
||||
derivedStateOf {
|
||||
when (mode) {
|
||||
is ShowLocationMode.Static -> updatedLocationShares.firstOrNull()
|
||||
is ShowLocationMode.Live -> updatedLocationShares.firstOrNull { it.userId == mode.senderId }
|
||||
}
|
||||
}
|
||||
}
|
||||
val isCurrentlySharing by liveLocationShareManager.isCurrentlySharing(roomId = joinedRoom.roomId).collectAsState()
|
||||
val hideUserLocationPuck = mode is ShowLocationMode.Live && isCurrentlySharing
|
||||
val userLocationState = if (hideUserLocationPuck) {
|
||||
// When sharing with this device, use the user LocationShareItem as source of data instead of the device.
|
||||
val ownLocationShare by remember { derivedStateOf { updatedLocationShares.find { it.isOwnUser }?.location?.asMapLibreLocation() } }
|
||||
UserLocationState(ownLocationShare)
|
||||
} else {
|
||||
userLocationStateFactory.create(hasLocationPermission = permissionsState.isAnyGranted)
|
||||
}
|
||||
|
||||
return ShowLocationState(
|
||||
customMapStyleUrl = customMapStyleUrl,
|
||||
dialogState = dialogState,
|
||||
locationShares = locationShares,
|
||||
focusedLocation = focusedLocation,
|
||||
hasLocationPermission = permissionsState.isAnyGranted,
|
||||
isTrackMyLocation = isTrackMyLocation,
|
||||
userLocationState = userLocationState,
|
||||
isLive = mode is ShowLocationMode.Live,
|
||||
appName = appName,
|
||||
hideUserLocationPuck = hideUserLocationPuck,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
+3
-1
@@ -11,6 +11,7 @@ package io.element.android.features.location.impl.show
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.ui.LocationMarkerData
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.PinVariant
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
@@ -24,9 +25,10 @@ data class ShowLocationState(
|
||||
val dialogState: LocationConstraintsDialogState,
|
||||
val locationShares: ImmutableList<LocationShareItem>,
|
||||
val focusedLocation: LocationShareItem?,
|
||||
val hasLocationPermission: Boolean,
|
||||
val isTrackMyLocation: Boolean,
|
||||
val userLocationState: UserLocationState,
|
||||
val appName: String,
|
||||
val hideUserLocationPuck: Boolean,
|
||||
val eventSink: (ShowLocationEvent) -> Unit,
|
||||
) {
|
||||
val isSheetDraggable = isLive && locationShares.isNotEmpty()
|
||||
|
||||
+7
-13
@@ -11,6 +11,7 @@ package io.element.android.features.location.impl.show
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
@@ -33,18 +34,9 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
||||
),
|
||||
aShowLocationState(
|
||||
constraintsDialogState = LocationConstraintsDialogState.LocationServiceDisabled,
|
||||
hasLocationPermission = true,
|
||||
),
|
||||
aShowLocationState(
|
||||
hasLocationPermission = true,
|
||||
),
|
||||
aShowLocationState(
|
||||
hasLocationPermission = true,
|
||||
isTrackMyLocation = true,
|
||||
),
|
||||
aShowLocationState(
|
||||
customMapStyleUrl = AsyncData.Loading(),
|
||||
),
|
||||
aShowLocationState(isTrackMyLocation = true),
|
||||
aShowLocationState(customMapStyleUrl = AsyncData.Loading()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,9 +48,10 @@ fun aShowLocationState(
|
||||
constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None,
|
||||
locationShares: List<LocationShareItem> = listOf(aLocationShareItem(isLive = isLive)),
|
||||
focusedLocation: LocationShareItem? = locationShares.firstOrNull(),
|
||||
hasLocationPermission: Boolean = false,
|
||||
isTrackMyLocation: Boolean = false,
|
||||
userLocationState: UserLocationState = UserLocationState(null),
|
||||
appName: String = APP_NAME,
|
||||
hideUserLocationPuck: Boolean = false,
|
||||
eventSink: (ShowLocationEvent) -> Unit = {},
|
||||
): ShowLocationState {
|
||||
return ShowLocationState(
|
||||
@@ -66,8 +59,9 @@ fun aShowLocationState(
|
||||
dialogState = constraintsDialogState,
|
||||
locationShares = locationShares.toImmutableList(),
|
||||
focusedLocation = focusedLocation,
|
||||
hasLocationPermission = hasLocationPermission,
|
||||
isTrackMyLocation = isTrackMyLocation,
|
||||
userLocationState = userLocationState,
|
||||
hideUserLocationPuck = hideUserLocationPuck,
|
||||
appName = appName,
|
||||
isLive = isLive,
|
||||
eventSink = eventSink,
|
||||
|
||||
+10
-5
@@ -37,13 +37,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.rememberUserLocationState
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialog
|
||||
import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton
|
||||
import io.element.android.features.location.impl.common.ui.LocationPinMarkers
|
||||
import io.element.android.features.location.impl.common.ui.LocationShareRow
|
||||
import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold
|
||||
import io.element.android.features.location.impl.common.ui.UserLocationPuck
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationTrackingEffect
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -91,7 +91,6 @@ fun ShowLocationView(
|
||||
}
|
||||
}
|
||||
|
||||
val userLocationState = rememberUserLocationState(state.hasLocationPermission)
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(SheetValue.Expanded)
|
||||
)
|
||||
@@ -169,11 +168,17 @@ fun ShowLocationView(
|
||||
}
|
||||
},
|
||||
mapContent = {
|
||||
UserLocationPuck(
|
||||
UserLocationTrackingEffect(
|
||||
cameraState = cameraState,
|
||||
locationState = userLocationState,
|
||||
trackUserLocation = state.isTrackMyLocation
|
||||
locationState = state.userLocationState,
|
||||
enabled = state.isTrackMyLocation,
|
||||
)
|
||||
if (!state.hideUserLocationPuck) {
|
||||
UserLocationPuck(
|
||||
cameraState = cameraState,
|
||||
location = state.userLocationState.location,
|
||||
)
|
||||
}
|
||||
val markers = remember(state.locationShares) {
|
||||
state.locationShares.map { it.toMarkerData() }.toImmutableList()
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
|
||||
class FakeUserLocationStateFactory : UserLocationState.Factory {
|
||||
@Composable
|
||||
override fun create(hasLocationPermission: Boolean): UserLocationState {
|
||||
return UserLocationState(null)
|
||||
}
|
||||
}
|
||||
+2
@@ -11,6 +11,7 @@ package io.element.android.features.location.impl.share
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
import io.element.android.features.location.impl.live.LiveLocationStore
|
||||
@@ -56,6 +57,7 @@ class DefaultShareLocationEntryPointTest {
|
||||
preferenceDataStoreFactory = FakePreferenceDataStoreFactory(),
|
||||
sessionId = room.sessionId,
|
||||
),
|
||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||
)
|
||||
},
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
|
||||
+2
-5
@@ -21,6 +21,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.aPermissionsState
|
||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||
@@ -102,6 +103,7 @@ class ShareLocationPresenterTest {
|
||||
durationFormatter = durationFormatter,
|
||||
liveLocationShareManager = liveLocationShareManager,
|
||||
liveLocationStore = liveLocationStore,
|
||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||
)
|
||||
|
||||
@Test
|
||||
@@ -118,7 +120,6 @@ class ShareLocationPresenterTest {
|
||||
val state = awaitFirstItem()
|
||||
assertThat(state.customMapStyleUrl.isLoading()).isFalse()
|
||||
assertThat(state.trackUserLocation).isTrue()
|
||||
assertThat(state.hasLocationPermission).isTrue()
|
||||
assertThat(state.dialogState).isEqualTo(ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.None))
|
||||
}
|
||||
}
|
||||
@@ -155,7 +156,6 @@ class ShareLocationPresenterTest {
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.trackUserLocation).isTrue()
|
||||
assertThat(initialState.hasLocationPermission).isTrue()
|
||||
assertThat(initialState.dialogState).isEqualTo(ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.None))
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,6 @@ class ShareLocationPresenterTest {
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.trackUserLocation).isFalse()
|
||||
assertThat(initialState.hasLocationPermission).isFalse()
|
||||
assertThat(initialState.dialogState).isEqualTo(
|
||||
ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied)
|
||||
)
|
||||
@@ -195,7 +194,6 @@ class ShareLocationPresenterTest {
|
||||
shareLocationPresenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.trackUserLocation).isFalse()
|
||||
assertThat(initialState.hasLocationPermission).isFalse()
|
||||
assertThat(initialState.dialogState).isEqualTo(
|
||||
ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale)
|
||||
)
|
||||
@@ -216,7 +214,6 @@ class ShareLocationPresenterTest {
|
||||
shareLocationPresenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.trackUserLocation).isFalse()
|
||||
assertThat(initialState.hasLocationPermission).isTrue()
|
||||
assertThat(initialState.dialogState).isEqualTo(
|
||||
ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled)
|
||||
)
|
||||
|
||||
-2
@@ -120,7 +120,6 @@ class ShareLocationViewTest {
|
||||
setShareLocationView(
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled),
|
||||
hasLocationPermission = true,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
navigateUp = EnsureNeverCalled(),
|
||||
@@ -135,7 +134,6 @@ class ShareLocationViewTest {
|
||||
setShareLocationView(
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled),
|
||||
hasLocationPermission = true,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
navigateUp = EnsureNeverCalled(),
|
||||
|
||||
+2
@@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||
import io.element.android.features.location.api.ShowLocationMode
|
||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
||||
@@ -51,6 +52,7 @@ class DefaultShowLocationEntryPointTest {
|
||||
joinedRoom = joinedRoom,
|
||||
client = FakeMatrixClient(),
|
||||
liveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||
)
|
||||
},
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
|
||||
+50
-57
@@ -15,6 +15,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.ShowLocationMode
|
||||
import io.element.android.features.location.impl.aPermissionsState
|
||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||
@@ -41,6 +42,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ShowLocationPresenterTest {
|
||||
@@ -76,26 +78,9 @@ class ShowLocationPresenterTest {
|
||||
joinedRoom = joinedRoom,
|
||||
client = client,
|
||||
liveLocationShareManager = liveLocationShareManager,
|
||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `emits initial state with no location permission`() = runTest {
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
)
|
||||
|
||||
val presenter = createShowLocationPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.customMapStyleUrl.isLoading()).isTrue()
|
||||
assertThat(initialState.hasLocationPermission).isFalse()
|
||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - non-null customMapStyleUrl`() = runTest {
|
||||
val shareLocationPresenter = createShowLocationPresenter(
|
||||
@@ -112,35 +97,6 @@ class ShowLocationPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits initial state location permission denied once`() = runTest {
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
)
|
||||
)
|
||||
|
||||
val presenter = createShowLocationPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasLocationPermission).isFalse()
|
||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits initial state with location permission`() = runTest {
|
||||
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||
|
||||
val presenter = createShowLocationPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasLocationPermission).isTrue()
|
||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits initial state with partial location permission`() = runTest {
|
||||
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
|
||||
@@ -148,7 +104,6 @@ class ShowLocationPresenterTest {
|
||||
val presenter = createShowLocationPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasLocationPermission).isTrue()
|
||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||
}
|
||||
}
|
||||
@@ -176,7 +131,6 @@ class ShowLocationPresenterTest {
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasLocationPermission).isTrue()
|
||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||
|
||||
initialState.eventSink(ShowLocationEvent.TrackMyLocation(true))
|
||||
@@ -184,7 +138,6 @@ class ShowLocationPresenterTest {
|
||||
|
||||
delay(1)
|
||||
|
||||
assertThat(trackMyLocationState.hasLocationPermission).isTrue()
|
||||
assertThat(trackMyLocationState.isTrackMyLocation).isTrue()
|
||||
|
||||
// Swipe the map to switch mode
|
||||
@@ -192,7 +145,6 @@ class ShowLocationPresenterTest {
|
||||
val trackLocationDisabledState = awaitItem()
|
||||
assertThat(trackLocationDisabledState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
|
||||
assertThat(trackLocationDisabledState.isTrackMyLocation).isFalse()
|
||||
assertThat(trackLocationDisabledState.hasLocationPermission).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,14 +167,12 @@ class ShowLocationPresenterTest {
|
||||
val trackLocationState = awaitItem()
|
||||
assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionRationale)
|
||||
assertThat(trackLocationState.isTrackMyLocation).isFalse()
|
||||
assertThat(trackLocationState.hasLocationPermission).isFalse()
|
||||
|
||||
// Dismiss the dialog
|
||||
initialState.eventSink(ShowLocationEvent.DismissDialog)
|
||||
val dialogDismissedState = awaitItem()
|
||||
assertThat(dialogDismissedState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
|
||||
assertThat(dialogDismissedState.isTrackMyLocation).isFalse()
|
||||
assertThat(dialogDismissedState.hasLocationPermission).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +194,6 @@ class ShowLocationPresenterTest {
|
||||
val trackLocationState = awaitItem()
|
||||
assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionRationale)
|
||||
assertThat(trackLocationState.isTrackMyLocation).isFalse()
|
||||
assertThat(trackLocationState.hasLocationPermission).isFalse()
|
||||
|
||||
// Continue the dialog sends permission request to the permissions presenter
|
||||
trackLocationState.eventSink(ShowLocationEvent.RequestPermissions)
|
||||
@@ -271,14 +220,12 @@ class ShowLocationPresenterTest {
|
||||
val trackLocationState = awaitItem()
|
||||
assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionDenied)
|
||||
assertThat(trackLocationState.isTrackMyLocation).isFalse()
|
||||
assertThat(trackLocationState.hasLocationPermission).isFalse()
|
||||
|
||||
// Dismiss the dialog
|
||||
initialState.eventSink(ShowLocationEvent.DismissDialog)
|
||||
val dialogDismissedState = awaitItem()
|
||||
assertThat(dialogDismissedState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
|
||||
assertThat(dialogDismissedState.isTrackMyLocation).isFalse()
|
||||
assertThat(dialogDismissedState.hasLocationPermission).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +273,6 @@ class ShowLocationPresenterTest {
|
||||
val presenter = createShowLocationPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasLocationPermission).isTrue()
|
||||
|
||||
// Try to track location when location services are disabled
|
||||
initialState.eventSink(ShowLocationEvent.TrackMyLocation(true))
|
||||
@@ -493,4 +439,51 @@ class ShowLocationPresenterTest {
|
||||
assertThat(state.isSheetDraggable).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `static mode never hides user location puck`() = runTest {
|
||||
val presenter = createShowLocationPresenter()
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.hideUserLocationPuck).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `live mode does not hide user location puck when not sharing`() = runTest {
|
||||
val presenter = createShowLocationPresenter(
|
||||
mode = ShowLocationMode.Live(senderId = A_USER_ID),
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.hideUserLocationPuck).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `live mode user location state uses own share position when sharing`() = runTest {
|
||||
val ownLiveLocationShare = aLiveLocationShare()
|
||||
val fakeRoom = FakeJoinedRoom(
|
||||
liveLocationSharesFlow = MutableStateFlow(listOf(ownLiveLocationShare))
|
||||
)
|
||||
val manager = FakeActiveLiveLocationShareManager(
|
||||
startShareLambda = { _, _ -> Result.success(Unit) }
|
||||
)
|
||||
manager.startShare(fakeRoom.roomId, 1.hours)
|
||||
|
||||
val presenter = createShowLocationPresenter(
|
||||
mode = ShowLocationMode.Live(senderId = A_USER_ID),
|
||||
joinedRoom = fakeRoom,
|
||||
liveLocationShareManager = manager,
|
||||
)
|
||||
val ownLocation = ownLiveLocationShare.lastLocation?.geoUri?.let(Location::fromGeoUri)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val state = awaitItem()
|
||||
assertThat(state.hideUserLocationPuck).isTrue()
|
||||
val location = requireNotNull(state.userLocationState.location)
|
||||
assertThat(location.position.value.latitude).isEqualTo(ownLocation?.lat)
|
||||
assertThat(location.position.value.longitude).isEqualTo(ownLocation?.lon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
Reference in New Issue
Block a user