Read map tiler custom style from matrix .well-known file

Closes #6844
This commit is contained in:
Benoit Marty
2026-05-26 15:25:48 +02:00
parent 1e67c2f77b
commit 4a90768893
20 changed files with 211 additions and 64 deletions
@@ -37,6 +37,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
@@ -44,9 +45,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import io.element.android.features.location.api.internal.rememberTileStyleUrl
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.map.MapOptions
@@ -63,6 +66,7 @@ import kotlin.math.roundToInt
* - Updating camera position padding based on sheet height
* - Rendering the MaplibreMap with proper ornament positioning
*
* @param customMapStyleUrl Optional custom style URL for the map
* @param modifier Modifier for the root layout
* @param scaffoldState State for the bottom sheet scaffold
* @param cameraState The camera state for the map
@@ -79,6 +83,7 @@ import kotlin.math.roundToInt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MapBottomSheetScaffold(
customMapStyleUrl: AsyncData<String?>,
modifier: Modifier = Modifier,
scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.PartiallyExpanded)
@@ -130,13 +135,22 @@ fun MapBottomSheetScaffold(
val ornamentOptions = mapOptions.ornamentOptions.copy(padding = sheetPadding)
val mapOptions = mapOptions.copy(ornamentOptions = ornamentOptions)
Box {
MaplibreMap(
options = mapOptions,
baseStyle = BaseStyle.Uri(rememberTileStyleUrl()),
modifier = Modifier.fillMaxSize(),
cameraState = cameraState,
content = mapContent,
)
when (customMapStyleUrl) {
is AsyncData.Success -> {
MaplibreMap(
options = mapOptions,
baseStyle = BaseStyle.Uri(rememberTileStyleUrl(customMapStyleUrl.data)),
modifier = Modifier.fillMaxSize(),
cameraState = cameraState,
content = mapContent,
)
}
else -> {
Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
}
overlayContent(sheetPadding)
}
}
@@ -14,6 +14,7 @@ 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.setValue
@@ -35,6 +36,7 @@ import io.element.android.features.location.impl.common.toDialogState
import io.element.android.features.location.impl.live.LiveLocationStore
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.core.extensions.flatMap
@@ -86,6 +88,10 @@ class ShareLocationPresenter(
}
val startLiveLocationAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val currentUser by client.userProfile.collectAsState()
val customMapStyleUrl by produceState(AsyncData.Loading()) {
// Ignore errors
value = AsyncData.Success(client.getMapStyleUrl().getOrNull())
}
val sendLiveLocationPermissions by room.permissionsAsState(SendLiveLocationPermissions.DEFAULT) { perms ->
perms.sendLiveLocationPermissions()
}
@@ -161,6 +167,7 @@ class ShareLocationPresenter(
}
return ShareLocationState(
customMapStyleUrl = customMapStyleUrl,
currentUser = currentUser,
dialogState = dialogState,
trackUserLocation = trackUserPosition,
@@ -10,10 +10,12 @@ package io.element.android.features.location.impl.share
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class ShareLocationState(
val customMapStyleUrl: AsyncData<String?>,
val currentUser: MatrixUser,
val dialogState: Dialog,
val trackUserLocation: Boolean,
@@ -11,6 +11,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.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.persistentListOf
@@ -76,10 +77,14 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
hasLocationPermission = true,
canShareLiveLocation = true,
),
aShareLocationState(
customMapStyleUrl = AsyncData.Loading(),
),
)
}
fun aShareLocationState(
customMapStyleUrl: AsyncData<String?> = AsyncData.Success(null),
currentUser: MatrixUser = MatrixUser(UserId("@user:matrix.org")),
dialogState: ShareLocationState.Dialog = ShareLocationState.Dialog.None,
trackUserPosition: Boolean = false,
@@ -90,6 +95,7 @@ fun aShareLocationState(
eventSink: (ShareLocationEvent) -> Unit = {},
): ShareLocationState {
return ShareLocationState(
customMapStyleUrl = customMapStyleUrl,
currentUser = currentUser,
dialogState = dialogState,
trackUserLocation = trackUserPosition,
@@ -117,6 +117,7 @@ fun ShareLocationView(
}
MapBottomSheetScaffold(
customMapStyleUrl = state.customMapStyleUrl,
cameraState = cameraState,
modifier = modifier,
scaffoldState = scaffoldState,
@@ -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.toDialogState
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.core.meta.BuildMeta
@@ -40,6 +41,7 @@ import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient
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.joinedRoomMembers
@@ -58,6 +60,7 @@ class ShowLocationPresenter(
private val buildMeta: BuildMeta,
private val dateFormatter: DateFormatter,
private val stringProvider: StringProvider,
private val client: MatrixClient,
private val joinedRoom: JoinedRoom,
private val liveLocationShareManager: ActiveLiveLocationShareManager,
) : Presenter<ShowLocationState> {
@@ -78,6 +81,11 @@ class ShowLocationPresenter(
mutableStateOf(LocationConstraintsDialogState.None)
}
val customMapStyleUrl by produceState(AsyncData.Loading()) {
// Ignore errors
value = AsyncData.Success(client.getMapStyleUrl().getOrNull())
}
LaunchedEffect(permissionsState.permissions) {
if (permissionsState.isAnyGranted) {
dialogState = LocationConstraintsDialogState.None
@@ -188,6 +196,7 @@ class ShowLocationPresenter(
}
return ShowLocationState(
customMapStyleUrl = customMapStyleUrl,
dialogState = dialogState,
locationShares = locationShares,
focusedLocation = focusedLocation,
@@ -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.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.PinVariant
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.UserId
@@ -18,6 +19,7 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
import kotlinx.collections.immutable.ImmutableList
data class ShowLocationState(
val customMapStyleUrl: AsyncData<String?>,
val isLive: Boolean,
val dialogState: LocationConstraintsDialogState,
val locationShares: ImmutableList<LocationShareItem>,
@@ -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.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
@@ -41,12 +42,16 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
hasLocationPermission = true,
isTrackMyLocation = true,
),
aShowLocationState(
customMapStyleUrl = AsyncData.Loading(),
),
)
}
private const val APP_NAME = "ApplicationName"
fun aShowLocationState(
customMapStyleUrl: AsyncData<String?> = AsyncData.Success(null),
isLive: Boolean = false,
constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None,
locationShares: List<LocationShareItem> = listOf(aLocationShareItem(isLive = isLive)),
@@ -57,6 +62,7 @@ fun aShowLocationState(
eventSink: (ShowLocationEvent) -> Unit = {},
): ShowLocationState {
return ShowLocationState(
customMapStyleUrl = customMapStyleUrl,
dialogState = constraintsDialogState,
locationShares = locationShares.toImmutableList(),
focusedLocation = focusedLocation,
@@ -100,6 +100,7 @@ fun ShowLocationView(
}
}
MapBottomSheetScaffold(
customMapStyleUrl = state.customMapStyleUrl,
sheetDragHandle = if (state.isSheetDraggable) {
{ BottomSheetDefaults.DragHandle() }
} else {