From 4a907688937c66049dab0e66de388fb4b33e2c41 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 26 May 2026 15:25:48 +0200 Subject: [PATCH] Read map tiler custom style from matrix .well-known file Closes #6844 --- .../MapTilerTileServerStyleUriBuilder.kt | 21 +++- .../api/internal/TileServerStyleUriBuilder.kt | 14 ++- .../MapTilerTileServerStyleUriBuilderTest.kt | 30 +++++- .../impl/common/ui/MapBottomSheetScaffold.kt | 28 ++++-- .../impl/share/ShareLocationPresenter.kt | 7 ++ .../location/impl/share/ShareLocationState.kt | 2 + .../impl/share/ShareLocationStateProvider.kt | 6 ++ .../location/impl/share/ShareLocationView.kt | 1 + .../impl/show/ShowLocationPresenter.kt | 9 ++ .../location/impl/show/ShowLocationState.kt | 2 + .../impl/show/ShowLocationStateProvider.kt | 6 ++ .../location/impl/show/ShowLocationView.kt | 1 + .../impl/share/ShareLocationPresenterTest.kt | 96 ++++++++++--------- .../show/DefaultShowLocationEntryPointTest.kt | 2 + .../impl/show/ShowLocationPresenterTest.kt | 29 +++++- gradle/libs.versions.toml | 2 +- .../libraries/matrix/api/MatrixClient.kt | 5 + .../libraries/matrix/impl/RustMatrixClient.kt | 6 ++ .../matrix/impl/pushers/RustPushersService.kt | 3 +- .../libraries/matrix/test/FakeMatrixClient.kt | 5 + 20 files changed, 211 insertions(+), 64 deletions(-) diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt index d119dc7eb2..761adb25cf 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt @@ -25,8 +25,23 @@ internal class MapTilerTileServerStyleUriBuilder( darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID, ) - override fun build(darkMode: Boolean): String { - val mapId = if (darkMode) darkMapId else lightMapId - return "$baseUrl/$mapId/style.json?key=$apiKey" + override fun build( + customMapStyleUrl: String?, + darkMode: Boolean, + ): String { + return buildString { + if (customMapStyleUrl.isNullOrBlank()) { + val mapId = if (darkMode) darkMapId else lightMapId + append(baseUrl) + append("/") + append(mapId) + append("/") + append("style.json") + } else { + append(customMapStyleUrl.removeSuffix("/")) + } + append("?key=") + append(apiKey) + } } } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt index 17b620eec9..762d580f8e 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt @@ -19,21 +19,25 @@ import io.element.android.compound.theme.ElementTheme */ interface TileServerStyleUriBuilder { fun build( + customMapStyleUrl: String?, darkMode: Boolean, ): String } -fun TileServerStyleUriBuilder(): TileServerStyleUriBuilder = MapTilerTileServerStyleUriBuilder() - /** * Provides and remembers a style URI for a MapLibre compatible tile server. * * Used for rendering dynamic maps. */ @Composable -fun rememberTileStyleUrl(): String { +fun rememberTileStyleUrl( + customMapStyleUrl: String?, +): String { val darkMode = !ElementTheme.isLightTheme - return remember(darkMode) { - TileServerStyleUriBuilder().build(darkMode) + return remember(darkMode, customMapStyleUrl) { + MapTilerTileServerStyleUriBuilder().build( + customMapStyleUrl = customMapStyleUrl, + darkMode = darkMode, + ) } } diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt index d5c6521258..2e07204594 100644 --- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt +++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt @@ -22,14 +22,40 @@ class MapTilerTileServerStyleUriBuilderTest { @Test fun `light map uri`() { assertThat( - builder.build(darkMode = false) + builder.build( + customMapStyleUrl = null, + darkMode = false, + ) ).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey") } @Test fun `dark map uri`() { assertThat( - builder.build(darkMode = true) + builder.build( + customMapStyleUrl = null, + darkMode = true, + ) ).isEqualTo("https://base.url/aDarkMapId/style.json?key=anApiKey") } + + @Test + fun `custom map uri light`() { + assertThat( + builder.build( + customMapStyleUrl = "https://custom.url/style.json", + darkMode = false, + ) + ).isEqualTo("https://custom.url/style.json?key=anApiKey") + } + + @Test + fun `custom map uri dark`() { + assertThat( + builder.build( + customMapStyleUrl = "https://custom.url/style.json", + darkMode = true, + ) + ).isEqualTo("https://custom.url/style.json?key=anApiKey") + } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt index 13c30c28eb..5f09bcedb2 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt @@ -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, 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) } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt index a1e45cfea2..9f4c8333b8 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt @@ -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.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, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt index 68598cba04..9e78345bab 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt @@ -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, val currentUser: MatrixUser, val dialogState: Dialog, val trackUserLocation: Boolean, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt index ae1b765b6b..e7942b12df 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt @@ -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 hasLocationPermission = true, canShareLiveLocation = true, ), + aShareLocationState( + customMapStyleUrl = AsyncData.Loading(), + ), ) } fun aShareLocationState( + customMapStyleUrl: AsyncData = 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, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt index e20ee3a7a5..985c6c8bdc 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt @@ -117,6 +117,7 @@ fun ShareLocationView( } MapBottomSheetScaffold( + customMapStyleUrl = state.customMapStyleUrl, cameraState = cameraState, modifier = modifier, scaffoldState = scaffoldState, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt index 207b4b01dd..bdb6dd26b8 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt @@ -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 { @@ -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, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt index 720697cbf7..478a3bd730 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt @@ -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, val isLive: Boolean, val dialogState: LocationConstraintsDialogState, val locationShares: ImmutableList, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt index 1c7b9a3160..85eacd3ce4 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt @@ -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 { hasLocationPermission = true, isTrackMyLocation = true, ), + aShowLocationState( + customMapStyleUrl = AsyncData.Loading(), + ), ) } private const val APP_NAME = "ApplicationName" fun aShowLocationState( + customMapStyleUrl: AsyncData = AsyncData.Success(null), isLive: Boolean = false, constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None, locationShares: List = 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, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt index 6766fa6424..80ffaabedd 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt @@ -100,6 +100,7 @@ fun ShowLocationView( } } MapBottomSheetScaffold( + customMapStyleUrl = state.customMapStyleUrl, sheetDragHandle = if (state.isSheetDraggable) { { BottomSheetDefaults.DragHandle() } } else { diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt index 1bc4fce586..5be96a50c5 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt @@ -15,6 +15,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.emptyPreferences import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow +import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Composer @@ -75,7 +76,10 @@ class ShareLocationPresenterTest { private val fakeMessageComposerContext = FakeMessageComposerContext() private val fakeLocationActions = FakeLocationActions() private val fakeBuildMeta = aBuildMeta(applicationName = "app name") - private val fakeMatrixClient = FakeMatrixClient(sessionId = A_USER_ID) + private val fakeMatrixClient = FakeMatrixClient( + sessionId = A_USER_ID, + getMapStyleUrlResult = { Result.success(null) }, + ) private val durationFormatter = FakeDurationFormatter() @@ -85,6 +89,7 @@ class ShareLocationPresenterTest { locationActions: FakeLocationActions = fakeLocationActions, liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), liveLocationStore: LiveLocationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId), + client: FakeMatrixClient = fakeMatrixClient, ): ShareLocationPresenter = ShareLocationPresenter( permissionsPresenterFactory = { fakePermissionsPresenter }, room = joinedRoom, @@ -93,7 +98,7 @@ class ShareLocationPresenterTest { messageComposerContext = fakeMessageComposerContext, locationActions = locationActions, buildMeta = fakeBuildMeta, - client = fakeMatrixClient, + client = client, durationFormatter = durationFormatter, liveLocationShareManager = liveLocationShareManager, liveLocationStore = liveLocationStore, @@ -110,14 +115,31 @@ class ShareLocationPresenterTest { val shareLocationPresenter = createShareLocationPresenter() shareLocationPresenter.test { - skipItems(1) - val state = awaitItem() + 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)) } } + @Test + fun `present - non-null customMapStyleUrl`() = runTest { + val shareLocationPresenter = createShareLocationPresenter( + client = FakeMatrixClient( + sessionId = A_USER_ID, + getMapStyleUrlResult = { Result.success("aUrl") }, + ) + ) + shareLocationPresenter.test { + skipItems(1) + val state = awaitItem() + assertThat(state.customMapStyleUrl.isLoading()).isTrue() + val finalState = awaitItem() + assertThat(finalState.customMapStyleUrl.dataOrNull()).isEqualTo("aUrl") + } + } + @Test fun `initial state with permissions partially granted and location enabled`() = runTest { val shareLocationPresenter = createShareLocationPresenter() @@ -131,8 +153,7 @@ class ShareLocationPresenterTest { moleculeFlow(RecompositionMode.Immediate) { shareLocationPresenter.present() }.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.trackUserLocation).isTrue() assertThat(initialState.hasLocationPermission).isTrue() assertThat(initialState.dialogState).isEqualTo(ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.None)) @@ -152,8 +173,7 @@ class ShareLocationPresenterTest { moleculeFlow(RecompositionMode.Immediate) { shareLocationPresenter.present() }.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.trackUserLocation).isFalse() assertThat(initialState.hasLocationPermission).isFalse() assertThat(initialState.dialogState).isEqualTo( @@ -173,8 +193,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.trackUserLocation).isFalse() assertThat(initialState.hasLocationPermission).isFalse() assertThat(initialState.dialogState).isEqualTo( @@ -195,8 +214,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.trackUserLocation).isFalse() assertThat(initialState.hasLocationPermission).isTrue() assertThat(initialState.dialogState).isEqualTo( @@ -216,8 +234,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.trackUserLocation).isTrue() initialState.eventSink(ShareLocationEvent.StopTrackingUserLocation) @@ -237,8 +254,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.dialogState).isEqualTo( ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale) ) @@ -260,7 +276,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink(ShareLocationEvent.RequestPermissions) // Wait for dialog to be dismissed @@ -282,8 +298,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink(ShareLocationEvent.OpenAppSettings) val settingsOpenedState = awaitItem() @@ -304,8 +319,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.dialogState).isEqualTo( ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled) ) @@ -337,8 +351,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink(ShareLocationEvent.InitiateLiveLocationShare) val durationDialogState = awaitItem() @@ -358,8 +371,7 @@ class ShareLocationPresenterTest { ) presenter.test { - skipItems(1) - val state = awaitItem() + val state = awaitFirstItem() state.eventSink(ShareLocationEvent.InitiateLiveLocationShare) val dialogState = awaitItem() @@ -385,8 +397,7 @@ class ShareLocationPresenterTest { ) presenter.test { - skipItems(1) - val state = awaitItem() + val state = awaitFirstItem() state.eventSink(ShareLocationEvent.InitiateLiveLocationShare) awaitItem() @@ -413,8 +424,7 @@ class ShareLocationPresenterTest { ) presenter.test { - skipItems(1) - val state = awaitItem() + val state = awaitFirstItem() state.eventSink(ShareLocationEvent.InitiateLiveLocationShare) val disclaimerState = awaitItem() @@ -444,8 +454,7 @@ class ShareLocationPresenterTest { ) presenter.test { - skipItems(1) - val state = awaitItem() + val state = awaitFirstItem() state.eventSink(ShareLocationEvent.InitiateLiveLocationShare) val durationState = awaitItem() @@ -472,8 +481,7 @@ class ShareLocationPresenterTest { ) presenter.test { - skipItems(1) - val state = awaitItem() + val state = awaitFirstItem() state.eventSink(ShareLocationEvent.InitiateLiveLocationShare) val dialogState = awaitItem() @@ -504,8 +512,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() // Dismiss initial dialog initialState.eventSink(ShareLocationEvent.DismissDialog) val dismissedState = awaitItem() @@ -539,8 +546,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink( ShareLocationEvent.ShareStaticLocation( @@ -593,7 +599,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink( ShareLocationEvent.ShareStaticLocation( @@ -641,8 +647,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val state = awaitItem() + val state = awaitFirstItem() state.eventSink(ShareLocationEvent.StartLiveLocationShare(duration = 1.hours)) advanceUntilIdle() assert(startShareLambda).isCalledOnce().with( @@ -659,8 +664,7 @@ class ShareLocationPresenterTest { timelineMode = Timeline.Mode.Live, ) shareLocationPresenter.test { - skipItems(1) - val state = awaitItem() + val state = awaitFirstItem() assertThat(state.canShareLiveLocation).isTrue() } } @@ -671,11 +675,15 @@ class ShareLocationPresenterTest { timelineMode = Timeline.Mode.Thread(A_THREAD_ID), ) shareLocationPresenter.test { - skipItems(1) - val state = awaitItem() + val state = awaitFirstItem() assertThat(state.canShareLiveLocation).isFalse() } } + + private suspend fun ReceiveTurbine.awaitFirstItem(): T { + skipItems(2) + return awaitItem() + } } private fun createLiveLocationStore( diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt index 985dcc1f9c..8f55510128 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt @@ -19,6 +19,7 @@ import io.element.android.features.location.impl.common.permissions.FakePermissi import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.services.analytics.test.FakeAnalyticsService @@ -48,6 +49,7 @@ class DefaultShowLocationEntryPointTest { dateFormatter = FakeDateFormatter(), stringProvider = FakeStringProvider(), joinedRoom = joinedRoom, + client = FakeMatrixClient(), liveLocationShareManager = FakeActiveLiveLocationShareManager(), ) }, diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt index c1f9f6487e..da3cea88f6 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt @@ -22,10 +22,13 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.libraries.dateformatter.test.FakeDateFormatter +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.location.LiveLocationShare +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.location.aLiveLocationShare @@ -61,6 +64,7 @@ class ShowLocationPresenterTest { ), locationActions: FakeLocationActions = fakeLocationActions, joinedRoom: JoinedRoom = FakeJoinedRoom(), + client: MatrixClient = FakeMatrixClient(), liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), ) = ShowLocationPresenter( mode = mode, @@ -70,6 +74,7 @@ class ShowLocationPresenterTest { dateFormatter = fakeDateFormatter, stringProvider = FakeStringProvider(), joinedRoom = joinedRoom, + client = client, liveLocationShareManager = liveLocationShareManager, ) @@ -85,11 +90,28 @@ class ShowLocationPresenterTest { 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( + client = FakeMatrixClient( + sessionId = A_USER_ID, + getMapStyleUrlResult = { Result.success("aUrl") }, + ) + ) + shareLocationPresenter.test { + val state = awaitItem() + assertThat(state.customMapStyleUrl.isLoading()).isTrue() + val finalState = awaitItem() + assertThat(finalState.customMapStyleUrl.dataOrNull()).isEqualTo("aUrl") + } + } + @Test fun `emits initial state location permission denied once`() = runTest { fakePermissionsPresenter.givenState( @@ -146,8 +168,13 @@ class ShowLocationPresenterTest { fun `centers on user location`() = runTest { fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted)) - val presenter = createShowLocationPresenter() + val presenter = createShowLocationPresenter( + client = FakeMatrixClient( + getMapStyleUrlResult = { Result.success(null) } + ) + ) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.hasLocationPermission).isTrue() assertThat(initialState.isTrackMyLocation).isFalse() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a328218d79..170504f0fe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -179,7 +179,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # All new features should not be implemented in the pull request that upgrades the version, developers should # only fix API breaks and may add some TODOs. -matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.05.20" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.05.26" # Others coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index bd7b1399e1..836459f8c7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -221,6 +221,11 @@ interface MatrixClient { */ suspend fun performDatabaseVacuum(): Result + /** + * Returns the URL of the map style configured on the server, if any. + */ + suspend fun getMapStyleUrl(): Result + /** * Resets the cached client `well-known` config by the SDK. */ diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 05e52605dc..bec20aedf2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -808,6 +808,12 @@ class RustMatrixClient( } } + override suspend fun getMapStyleUrl(): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.tileServer()?.mapStyleUrl + } + } + override suspend fun resetWellKnownConfig(): Result { return runCatchingExceptions { Timber.d("Resetting well-known config for session $sessionId") diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt index 860993d4bc..03a4ca3281 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt @@ -44,7 +44,8 @@ class RustPushersService( appDisplayName = setHttpPusherData.appDisplayName, deviceDisplayName = setHttpPusherData.deviceDisplayName, profileTag = setHttpPusherData.profileTag, - lang = setHttpPusherData.lang + lang = setHttpPusherData.lang, + append = false, ) } .mapFailure { it.mapClientException() } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index af28dc37e4..3278eae855 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -115,6 +115,7 @@ class FakeMatrixClient( private val addRecentEmojiLambda: (String) -> Result = { Result.success(Unit) }, private val markRoomAsFullyReadResult: (RoomId, EventId) -> Result = { _, _ -> lambdaError() }, private val performDatabaseVacuumLambda: () -> Result = { lambdaError() }, + private val getMapStyleUrlResult: () -> Result = { lambdaError() }, private val getDatabaseSizesLambda: () -> Result = { lambdaError() }, private val resetWellKnownConfigLambda: () -> Result = { lambdaError() }, ) : MatrixClient { @@ -373,6 +374,10 @@ class FakeMatrixClient( return performDatabaseVacuumLambda() } + override suspend fun getMapStyleUrl(): Result = simulateLongTask { + getMapStyleUrlResult() + } + override suspend fun canLinkNewDevice(): Result = simulateLongTask { return canLinkNewDeviceResult() }