Improve live location UI with empty state
This commit is contained in:
parent
9977370332
commit
775d5692aa
5 changed files with 56 additions and 33 deletions
|
|
@ -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 = {},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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?,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue