Move the custom tile server URL config to the Element wellknown content (#7416)

* Move the custom tile server URL config to the Element wellknown content

* Iteration: use whole `MapTilerConfig` in the remote config
This commit is contained in:
Jorge Martin Espinosa
2026-08-14 08:20:37 +02:00
committed by GitHub
parent ddbc6a2a33
commit 22195a47f2
35 changed files with 279 additions and 117 deletions
+1
View File
@@ -70,6 +70,7 @@ dependencies {
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.wellknown.api)
implementation(libs.coil.compose)
implementation(libs.datetime)
@@ -0,0 +1,23 @@
/*
* 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.api
import androidx.compose.runtime.staticCompositionLocalOf
import io.element.android.libraries.wellknown.api.MapTilerConfig
/**
* Provides a [MapTilerConfig] for the MapLibre maps.
*/
val LocalMapTilerConfig = staticCompositionLocalOf {
MapTilerConfig(
apiKey = BuildConfig.MAPTILER_API_KEY,
lightStyleId = BuildConfig.MAPTILER_LIGHT_MAP_ID,
darkStyleId = BuildConfig.MAPTILER_DARK_MAP_ID,
baseUrl = BuildConfig.MAPTILER_BASE_URL
)
}
@@ -38,13 +38,14 @@ import coil3.request.ImageRequest
import coil3.request.SuccessResult
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.location.api.internal.StaticMapPlaceholder
import io.element.android.features.location.api.internal.StaticMapUrlBuilder
import io.element.android.features.location.api.internal.centerBottomEdge
import io.element.android.features.location.api.internal.rememberStaticMapBuilder
import io.element.android.libraries.designsystem.components.LocationPin
import io.element.android.libraries.designsystem.components.PinVariant
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.wellknown.api.MapTilerConfig
/**
* Shows a static map image downloaded via a third party service's static maps API.
@@ -57,6 +58,7 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables
*/
@Composable
fun StaticMapView(
mapTilerConfig: MapTilerConfig?,
location: Location?,
zoom: Double,
pinVariant: PinVariant,
@@ -94,6 +96,7 @@ fun StaticMapView(
}
// Cases 3 & 4: Non-null location - fetch map
else -> LoadableMapContent(
mapTilerConfig = mapTilerConfig,
location = location,
zoom = zoom,
pinVariant = pinVariant,
@@ -124,6 +127,7 @@ private fun BoxWithConstraintsScope.StaleMapContent(
@Composable
private fun BoxWithConstraintsScope.LoadableMapContent(
mapTilerConfig: MapTilerConfig?,
location: Location,
zoom: Double,
pinVariant: PinVariant,
@@ -132,7 +136,7 @@ private fun BoxWithConstraintsScope.LoadableMapContent(
) {
val context = LocalContext.current
var retryHash by remember { mutableIntStateOf(0) }
val builder = remember { StaticMapUrlBuilder() }
val builder = rememberStaticMapBuilder(mapTilerConfig)
val (painter, state, contentScale) = if (LocalInspectionMode.current) {
val painter = painterResource(CommonDrawables.sample_map)
@@ -203,6 +207,7 @@ private fun BoxWithConstraintsScope.LoadableMapContent(
@Composable
internal fun StaticMapViewPreview() = ElementPreview {
StaticMapView(
mapTilerConfig = null,
location = Location(0.0, 0.0),
zoom = 0.0,
contentDescription = null,
@@ -9,6 +9,7 @@
package io.element.android.features.location.api.internal
import io.element.android.features.location.api.BuildConfig
import io.element.android.libraries.wellknown.api.MapTilerConfig
import kotlin.math.roundToInt
/**
@@ -29,6 +30,13 @@ internal class MapTilerStaticMapUrlBuilder(
darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID,
)
constructor(mapTilerConfig: MapTilerConfig) : this(
baseUrl = (mapTilerConfig.baseUrl ?: BuildConfig.MAPTILER_BASE_URL).removeSuffix("/"),
apiKey = mapTilerConfig.apiKey.takeIf { it.isNotBlank() } ?: BuildConfig.MAPTILER_API_KEY,
lightMapId = mapTilerConfig.lightStyleId ?: BuildConfig.MAPTILER_LIGHT_MAP_ID,
darkMapId = mapTilerConfig.darkStyleId ?: BuildConfig.MAPTILER_DARK_MAP_ID,
)
override fun build(
lat: Double,
lon: Double,
@@ -11,6 +11,7 @@
package io.element.android.features.location.api.internal
import io.element.android.features.location.api.BuildConfig
import io.element.android.libraries.wellknown.api.MapTilerConfig
internal class MapTilerTileServerStyleUriBuilder(
private val baseUrl: String,
@@ -26,14 +27,17 @@ internal class MapTilerTileServerStyleUriBuilder(
)
override fun build(
customMapStyleUrl: String?,
customMapTilerConfig: MapTilerConfig?,
darkMode: Boolean,
): String {
return if (customMapStyleUrl.isNullOrBlank()) {
val mapId = if (darkMode) darkMapId else lightMapId
"$baseUrl/$mapId/style.json?key=$apiKey"
val baseUrl = customMapTilerConfig?.baseUrl.takeIf { !it.isNullOrBlank() } ?: baseUrl
val apiKey = customMapTilerConfig?.apiKey ?: apiKey
val mapId = if (darkMode) {
customMapTilerConfig?.darkStyleId ?: darkMapId
} else {
customMapStyleUrl
customMapTilerConfig?.lightStyleId ?: lightMapId
}
return "$baseUrl/$mapId/style.json?key=$apiKey"
}
}
@@ -8,6 +8,10 @@
package io.element.android.features.location.api.internal
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import io.element.android.libraries.wellknown.api.MapTilerConfig
/**
* Builds an URL for a 3rd party service provider static maps API.
*/
@@ -25,4 +29,13 @@ interface StaticMapUrlBuilder {
fun isServiceAvailable(): Boolean
}
fun StaticMapUrlBuilder(): StaticMapUrlBuilder = MapTilerStaticMapUrlBuilder()
@Composable
fun rememberStaticMapBuilder(mapTilerConfig: MapTilerConfig?): StaticMapUrlBuilder {
return remember(mapTilerConfig) {
if (mapTilerConfig != null) {
MapTilerStaticMapUrlBuilder(mapTilerConfig)
} else {
MapTilerStaticMapUrlBuilder()
}
}
}
@@ -11,6 +11,7 @@ package io.element.android.features.location.api.internal
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.wellknown.api.MapTilerConfig
/**
* Builds a style URI for a MapLibre compatible tile server.
@@ -19,7 +20,7 @@ import io.element.android.compound.theme.ElementTheme
*/
interface TileServerStyleUriBuilder {
fun build(
customMapStyleUrl: String?,
customMapTilerConfig: MapTilerConfig?,
darkMode: Boolean,
): String
}
@@ -31,12 +32,12 @@ interface TileServerStyleUriBuilder {
*/
@Composable
fun rememberTileStyleUrl(
customMapStyleUrl: String?,
customMapTileConfig: MapTilerConfig?,
): String {
val darkMode = !ElementTheme.isLightTheme
return remember(darkMode, customMapStyleUrl) {
return remember(darkMode, customMapTileConfig) {
MapTilerTileServerStyleUriBuilder().build(
customMapStyleUrl = customMapStyleUrl,
customMapTilerConfig = customMapTileConfig,
darkMode = darkMode,
)
}
@@ -9,6 +9,7 @@
package io.element.android.features.location.api.internal
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.wellknown.api.MapTilerConfig
import org.junit.Test
class MapTilerTileServerStyleUriBuilderTest {
@@ -23,7 +24,7 @@ class MapTilerTileServerStyleUriBuilderTest {
fun `light map uri`() {
assertThat(
builder.build(
customMapStyleUrl = null,
customMapTilerConfig = null,
darkMode = false,
)
).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey")
@@ -33,7 +34,7 @@ class MapTilerTileServerStyleUriBuilderTest {
fun `dark map uri`() {
assertThat(
builder.build(
customMapStyleUrl = null,
customMapTilerConfig = null,
darkMode = true,
)
).isEqualTo("https://base.url/aDarkMapId/style.json?key=anApiKey")
@@ -43,49 +44,44 @@ class MapTilerTileServerStyleUriBuilderTest {
fun `custom map uri light`() {
assertThat(
builder.build(
customMapStyleUrl = "https://custom.url/style.json",
customMapTilerConfig = MapTilerConfig(
apiKey = "API_KEY",
lightStyleId = "light",
darkStyleId = "dark",
baseUrl = "https://custom.url/style.json"
),
darkMode = false,
)
).isEqualTo("https://custom.url/style.json")
).isEqualTo("https://custom.url/style.json/light/style.json?key=API_KEY")
}
@Test
fun `custom map uri dark`() {
assertThat(
builder.build(
customMapStyleUrl = "https://custom.url/style.json",
customMapTilerConfig = MapTilerConfig(
apiKey = "API_KEY",
lightStyleId = "light",
darkStyleId = "dark",
baseUrl = "https://custom.url/style.json"
),
darkMode = true,
)
).isEqualTo("https://custom.url/style.json")
}
@Test
fun `custom map uri with its own api key is not altered`() {
assertThat(
builder.build(
customMapStyleUrl = "https://api.maptiler.com/maps/streets-v2/style.json?key=anOperatorKey",
darkMode = false,
)
).isEqualTo("https://api.maptiler.com/maps/streets-v2/style.json?key=anOperatorKey")
}
@Test
fun `custom map uri with an existing query is not altered`() {
assertThat(
builder.build(
customMapStyleUrl = "https://self.hosted/style.json?foo=bar",
darkMode = true,
)
).isEqualTo("https://self.hosted/style.json?foo=bar")
).isEqualTo("https://custom.url/style.json/dark/style.json?key=API_KEY")
}
@Test
fun `blank custom map uri falls back to the built-in style`() {
assertThat(
builder.build(
customMapStyleUrl = " ",
customMapTilerConfig = MapTilerConfig(
apiKey = "API_KEY",
lightStyleId = "light",
darkStyleId = "dark",
baseUrl = " "
),
darkMode = false,
)
).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey")
).isEqualTo("https://base.url/light/style.json?key=API_KEY")
}
}