Improve live location UI with empty state

This commit is contained in:
ganfra 2026-04-10 20:44:05 +02:00
parent 9977370332
commit 775d5692aa
5 changed files with 56 additions and 33 deletions

View file

@ -91,7 +91,7 @@ fun LocationShareRow(
) )
} }
Text( Text(
text = item.formattedTimestamp, text = item.description,
style = ElementTheme.typography.fontBodySmRegular, style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
maxLines = 1, maxLines = 1,
@ -123,7 +123,7 @@ internal fun LocationShareRowPreview() = ElementPreview {
url = null, url = null,
size = AvatarSize.UserListItem, size = AvatarSize.UserListItem,
), ),
formattedTimestamp = "Shared 1 min ago", description = "Shared 1 min ago",
isLive = true, isLive = true,
assetType = AssetType.SENDER, assetType = AssetType.SENDER,
location = Location(0.0, 0.0) location = Location(0.0, 0.0)
@ -142,7 +142,7 @@ internal fun LocationShareRowPreview() = ElementPreview {
), ),
isLive = false, isLive = false,
assetType = AssetType.PIN, assetType = AssetType.PIN,
formattedTimestamp = "Shared 5 hours ago", description = "Shared 5 hours ago",
location = Location(0.0, 0.0) location = Location(0.0, 0.0)
), ),
onShareClick = {}, onShareClick = {},

View file

@ -40,7 +40,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.getBestName import io.element.android.libraries.matrix.api.room.getBestName
import io.element.android.libraries.matrix.api.room.joinedRoomMembers import io.element.android.libraries.matrix.api.room.joinedRoomMembers
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -124,7 +123,7 @@ class ShowLocationPresenter(
url = mode.senderAvatarUrl, url = mode.senderAvatarUrl,
size = AvatarSize.UserListItem, size = AvatarSize.UserListItem,
), ),
formattedTimestamp = formattedTimestamp, description = formattedTimestamp,
location = mode.location, location = mode.location,
isLive = false, isLive = false,
assetType = mode.assetType, assetType = mode.assetType,
@ -152,7 +151,7 @@ class ShowLocationPresenter(
url = avatarUrl, url = avatarUrl,
size = AvatarSize.UserListItem, size = AvatarSize.UserListItem,
), ),
formattedTimestamp = "Sharing live location", description = "Sharing live location",
location = location, location = location,
isLive = true, isLive = true,
assetType = lastLocation.assetType, assetType = lastLocation.assetType,
@ -169,6 +168,7 @@ class ShowLocationPresenter(
locationShares = locationShares, locationShares = locationShares,
hasLocationPermission = permissionsState.isAnyGranted, hasLocationPermission = permissionsState.isAnyGranted,
isTrackMyLocation = isTrackMyLocation, isTrackMyLocation = isTrackMyLocation,
isLive = mode is ShowLocationMode.Live,
appName = appName, appName = appName,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )

View file

@ -9,6 +9,7 @@
package io.element.android.features.location.impl.show package io.element.android.features.location.impl.show
import io.element.android.features.location.api.Location import io.element.android.features.location.api.Location
import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState 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.ui.LocationMarkerData
import io.element.android.libraries.designsystem.components.PinVariant import io.element.android.libraries.designsystem.components.PinVariant
@ -18,6 +19,7 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class ShowLocationState( data class ShowLocationState(
val isLive: Boolean,
val dialogState: LocationConstraintsDialogState, val dialogState: LocationConstraintsDialogState,
val locationShares: ImmutableList<LocationShareItem>, val locationShares: ImmutableList<LocationShareItem>,
val hasLocationPermission: Boolean, val hasLocationPermission: Boolean,
@ -25,14 +27,14 @@ data class ShowLocationState(
val appName: String, val appName: String,
val eventSink: (ShowLocationEvent) -> Unit, val eventSink: (ShowLocationEvent) -> Unit,
) { ) {
val isSheetDraggable = locationShares.any { item -> item.isLive } val isSheetDraggable = isLive && locationShares.isNotEmpty()
} }
data class LocationShareItem( data class LocationShareItem(
val userId: UserId, val userId: UserId,
val displayName: String, val displayName: String,
val avatarData: AvatarData, val avatarData: AvatarData,
val formattedTimestamp: String, val description: String,
val location: Location, val location: Location,
val isLive: Boolean, val isLive: Boolean,
val assetType: AssetType?, val assetType: AssetType?,

View file

@ -10,6 +10,7 @@ package io.element.android.features.location.impl.show
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.location.api.Location import io.element.android.features.location.api.Location
import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@ -21,6 +22,8 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
override val values: Sequence<ShowLocationState> override val values: Sequence<ShowLocationState>
get() = sequenceOf( get() = sequenceOf(
aShowLocationState(), aShowLocationState(),
aShowLocationState(isLive = true),
aShowLocationState(isLive = true, locationShares = emptyList()),
aShowLocationState( aShowLocationState(
constraintsDialogState = LocationConstraintsDialogState.PermissionDenied, constraintsDialogState = LocationConstraintsDialogState.PermissionDenied,
), ),
@ -44,8 +47,9 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
private const val APP_NAME = "ApplicationName" private const val APP_NAME = "ApplicationName"
fun aShowLocationState( fun aShowLocationState(
isLive: Boolean = false,
constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None, constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None,
locationShares: List<LocationShareItem> = listOf(aLocationShareItem()), locationShares: List<LocationShareItem> = listOf(aLocationShareItem(isLive = isLive)),
hasLocationPermission: Boolean = false, hasLocationPermission: Boolean = false,
isTrackMyLocation: Boolean = false, isTrackMyLocation: Boolean = false,
appName: String = APP_NAME, appName: String = APP_NAME,
@ -57,6 +61,7 @@ fun aShowLocationState(
hasLocationPermission = hasLocationPermission, hasLocationPermission = hasLocationPermission,
isTrackMyLocation = isTrackMyLocation, isTrackMyLocation = isTrackMyLocation,
appName = appName, appName = appName,
isLive = isLive,
eventSink = eventSink, eventSink = eventSink,
) )
} }
@ -78,7 +83,7 @@ fun aLocationShareItem(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,
avatarData = avatarData, avatarData = avatarData,
formattedTimestamp = formattedTimestamp, description = formattedTimestamp,
location = location, location = location,
isLive = isLive, isLive = isLive,
assetType = assetType, assetType = assetType,

View file

@ -12,6 +12,7 @@ package io.element.android.features.location.impl.show
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.BottomSheetDefaults
@ -26,6 +27,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
@ -88,7 +90,7 @@ fun ShowLocationView(
bottomSheetState = rememberStandardBottomSheetState( bottomSheetState = rememberStandardBottomSheetState(
initialValue = initialValue =
if (state.isSheetDraggable) { if (state.isSheetDraggable) {
SheetValue.PartiallyExpanded SheetValue.Expanded
} else { } else {
SheetValue.Expanded SheetValue.Expanded
} }
@ -116,29 +118,43 @@ fun ShowLocationView(
}, },
sheetContent = { sheetPaddings -> sheetContent = { sheetPaddings ->
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
Spacer(Modifier.height(20.dp)) if (!state.isSheetDraggable) {
Text( Spacer(Modifier.height(20.dp))
text = stringResource(CommonStrings.screen_static_location_sheet_title), }
style = ElementTheme.typography.fontBodyLgMedium, if (state.locationShares.isEmpty()) {
color = ElementTheme.colors.textPrimary, Spacer(Modifier.height(16.dp))
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), Text(
) text = "Nobody is sharing their location",
state.locationShares.forEach { locationShare -> style = ElementTheme.typography.fontBodyLgMedium,
LocationShareRow( color = ElementTheme.colors.textPrimary,
item = locationShare, modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
onShareClick = { state.eventSink(ShowLocationEvent.Share(locationShare.location)) }, textAlign = TextAlign.Center,
modifier = Modifier.clickable {
state.eventSink(ShowLocationEvent.TrackMyLocation(false))
val position = CameraPosition(
padding = sheetPaddings,
target = Position(locationShare.location.lon, locationShare.location.lat),
zoom = MapDefaults.DEFAULT_ZOOM
)
coroutineScope.launch {
cameraState.animateTo(finalPosition = position)
}
}
) )
Spacer(Modifier.height(16.dp))
} else {
Text(
text = stringResource(CommonStrings.screen_static_location_sheet_title),
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textPrimary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
state.locationShares.forEach { locationShare ->
LocationShareRow(
item = locationShare,
onShareClick = { state.eventSink(ShowLocationEvent.Share(locationShare.location)) },
modifier = Modifier.clickable {
state.eventSink(ShowLocationEvent.TrackMyLocation(false))
val position = CameraPosition(
padding = sheetPaddings,
target = Position(locationShare.location.lon, locationShare.location.lat),
zoom = MapDefaults.DEFAULT_ZOOM
)
coroutineScope.launch {
cameraState.animateTo(finalPosition = position)
}
}
)
}
} }
}, },
mapContent = { mapContent = {