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
@@ -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)
}
}
}
@@ -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,
)
}
}
@@ -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")
}
}
@@ -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 {
@@ -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 <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(2)
return awaitItem()
}
}
private fun createLiveLocationStore(
@@ -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(),
)
},
@@ -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()
+1 -1
View File
@@ -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" }
@@ -221,6 +221,11 @@ interface MatrixClient {
*/
suspend fun performDatabaseVacuum(): Result<Unit>
/**
* Returns the URL of the map style configured on the server, if any.
*/
suspend fun getMapStyleUrl(): Result<String?>
/**
* Resets the cached client `well-known` config by the SDK.
*/
@@ -808,6 +808,12 @@ class RustMatrixClient(
}
}
override suspend fun getMapStyleUrl(): Result<String?> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerClient.tileServer()?.mapStyleUrl
}
}
override suspend fun resetWellKnownConfig(): Result<Unit> {
return runCatchingExceptions {
Timber.d("Resetting well-known config for session $sessionId")
@@ -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() }
@@ -115,6 +115,7 @@ class FakeMatrixClient(
private val addRecentEmojiLambda: (String) -> Result<Unit> = { Result.success(Unit) },
private val markRoomAsFullyReadResult: (RoomId, EventId) -> Result<Unit> = { _, _ -> lambdaError() },
private val performDatabaseVacuumLambda: () -> Result<Unit> = { lambdaError() },
private val getMapStyleUrlResult: () -> Result<String?> = { lambdaError() },
private val getDatabaseSizesLambda: () -> Result<SdkStoreSizes> = { lambdaError() },
private val resetWellKnownConfigLambda: () -> Result<Unit> = { lambdaError() },
) : MatrixClient {
@@ -373,6 +374,10 @@ class FakeMatrixClient(
return performDatabaseVacuumLambda()
}
override suspend fun getMapStyleUrl(): Result<String?> = simulateLongTask {
getMapStyleUrlResult()
}
override suspend fun canLinkNewDevice(): Result<Boolean> = simulateLongTask {
return canLinkNewDeviceResult()
}