diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml
deleted file mode 100644
index dd650c15e1..0000000000
--- a/.idea/dictionaries/bmarty.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
- homeserver
-
-
-
\ No newline at end of file
diff --git a/.idea/dictionaries/shared.xml b/.idea/dictionaries/shared.xml
index 7c04ccd5e7..9353e11fd9 100644
--- a/.idea/dictionaries/shared.xml
+++ b/.idea/dictionaries/shared.xml
@@ -2,8 +2,10 @@
backstack
+ homeserver
kover
onboarding
+ showkase
textfields
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
index 78c39f93e6..733485fcef 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
@@ -44,6 +44,7 @@ import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView
import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.api.oidc.OidcActionFlow
+import io.element.android.features.preferences.api.CacheService
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
@@ -54,7 +55,9 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
@@ -65,6 +68,7 @@ class RootFlowNode @AssistedInject constructor(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List,
private val authenticationService: MatrixAuthenticationService,
+ private val cacheService: CacheService,
private val matrixClientsHolder: MatrixClientsHolder,
private val presenter: RootPresenter,
private val bugReportEntryPoint: BugReportEntryPoint,
@@ -88,11 +92,19 @@ class RootFlowNode @AssistedInject constructor(
private fun observeLoggedInState() {
authenticationService.isLoggedIn()
.distinctUntilChanged()
- .onEach { isLoggedIn ->
- Timber.v("isLoggedIn=$isLoggedIn")
+ .combine(
+ cacheService.cacheIndex().onEach {
+ Timber.v("cacheIndex=$it")
+ matrixClientsHolder.removeAll()
+ }
+ ) { isLoggedIn, cacheIdx -> isLoggedIn to cacheIdx }
+ .onEach { pair ->
+ val isLoggedIn = pair.first
+ val cacheIndex = pair.second
+ Timber.v("isLoggedIn=$isLoggedIn, cacheIndex=$cacheIndex")
if (isLoggedIn) {
tryToRestoreLatestSession(
- onSuccess = { switchToLoggedInFlow(it) },
+ onSuccess = { switchToLoggedInFlow(it, cacheIndex) },
onFailure = { switchToNotLoggedInFlow() }
)
} else {
@@ -102,8 +114,8 @@ class RootFlowNode @AssistedInject constructor(
.launchIn(lifecycleScope)
}
- private fun switchToLoggedInFlow(sessionId: SessionId) {
- backstack.safeRoot(NavTarget.LoggedInFlow(sessionId))
+ private fun switchToLoggedInFlow(sessionId: SessionId, cacheIndex: Int) {
+ backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, cacheIndex))
}
private fun switchToNotLoggedInFlow() {
@@ -163,7 +175,7 @@ class RootFlowNode @AssistedInject constructor(
object NotLoggedInFlow : NavTarget
@Parcelize
- data class LoggedInFlow(val sessionId: SessionId) : NavTarget
+ data class LoggedInFlow(val sessionId: SessionId, val cacheIndex: Int) : NavTarget
@Parcelize
object BugReport : NavTarget
@@ -235,8 +247,9 @@ class RootFlowNode @AssistedInject constructor(
}
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
+ val cacheIndex = cacheService.cacheIndex().first()
return attachChild {
- backstack.newRoot(NavTarget.LoggedInFlow(sessionId))
+ backstack.newRoot(NavTarget.LoggedInFlow(sessionId, cacheIndex))
}
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 240af6c9ae..b0bcde29a8 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -247,6 +247,7 @@ koverMerged {
excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*"
excludes += "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState"
excludes += "io.element.android.features.messages.impl.media.local.LocalMediaViewState"
+ excludes += "io.element.android.features.location.api.MapState"
}
bound {
minValue = 90
diff --git a/features/analytics/impl/src/main/res/values-sk/translations.xml b/features/analytics/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..5f494909af
--- /dev/null
+++ b/features/analytics/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,7 @@
+
+
+ "Nezaznamenávame ani neprofilujeme žiadne osobné údaje"
+ "Môžete to kedykoľvek vypnúť"
+ "Vaše údaje nebudeme zdieľať s tretími stranami"
+ "Pomôžte zlepšiť %1$s"
+
diff --git a/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt b/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt
index 3d969567f7..7ff0f50d9c 100644
--- a/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt
+++ b/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt
@@ -29,9 +29,9 @@ class FakeAnalyticsService(
didAskUserConsent: Boolean = false
): AnalyticsService {
- private var isEnabledFlow = MutableStateFlow(isEnabled)
- private var didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
- var capturedEvents = mutableListOf()
+ private val isEnabledFlow = MutableStateFlow(isEnabled)
+ private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
+ val capturedEvents = mutableListOf()
override fun getAvailableAnalyticsProviders(): List = emptyList()
diff --git a/features/createroom/impl/src/main/res/values-de/translations.xml b/features/createroom/impl/src/main/res/values-de/translations.xml
index 5344a7f52d..abc2ef9d71 100644
--- a/features/createroom/impl/src/main/res/values-de/translations.xml
+++ b/features/createroom/impl/src/main/res/values-de/translations.xml
@@ -2,7 +2,7 @@
"Neuer Raum"
"Freunde zu Element einladen"
- "Personen einladen"
+ "Personen hinzufügen"
"Beim Erstellen des Raums ist ein Fehler aufgetreten"
"Die Nachrichten in diesem Raum sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden."
"Privater Raum (nur auf Einladung)"
diff --git a/features/createroom/impl/src/main/res/values-sk/translations.xml b/features/createroom/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..1b81b93561
--- /dev/null
+++ b/features/createroom/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,14 @@
+
+
+ "Nová miestnosť"
+ "Pozvať priateľov na Element"
+ "Pozvať ľudí"
+ "Pri vytváraní miestnosti došlo k chybe"
+ "Správy v tejto miestnosti sú šifrované. Šifrovanie už potom nie je možné vypnúť."
+ "Súkromná miestnosť (len pre pozvaných)"
+ "Správy nie sú šifrované a môže si ich prečítať ktokoľvek. Šifrovanie môžete zapnúť neskôr."
+ "Verejná miestnosť (ktokoľvek)"
+ "Názov miestnosti"
+ "Téma (voliteľné)"
+ "Vytvoriť miestnosť"
+
diff --git a/features/invitelist/impl/src/main/res/values-sk/translations.xml b/features/invitelist/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..fa3cf45ead
--- /dev/null
+++ b/features/invitelist/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,6 @@
+
+
+ "Odmietnuť pozvanie"
+ "Žiadne pozvánky"
+ "%1$s (%2$s) vás pozval/a"
+
diff --git a/features/invitelist/test/src/main/kotlin/io/element/android/features/invitelist/test/FakeSeenInvitesStore.kt b/features/invitelist/test/src/main/kotlin/io/element/android/features/invitelist/test/FakeSeenInvitesStore.kt
index 3716cd4456..486d3fb4a8 100644
--- a/features/invitelist/test/src/main/kotlin/io/element/android/features/invitelist/test/FakeSeenInvitesStore.kt
+++ b/features/invitelist/test/src/main/kotlin/io/element/android/features/invitelist/test/FakeSeenInvitesStore.kt
@@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
class FakeSeenInvitesStore : SeenInvitesStore {
- private var existing = MutableStateFlow(emptySet())
+ private val existing = MutableStateFlow(emptySet())
private var provided: Set? = null
fun publishRoomIds(invites: Set) {
diff --git a/features/location/api/build.gradle.kts b/features/location/api/build.gradle.kts
new file mode 100644
index 0000000000..a2f73e7def
--- /dev/null
+++ b/features/location/api/build.gradle.kts
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("io.element.android-compose-library")
+ alias(libs.plugins.anvil)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.ksp)
+}
+
+android {
+ namespace = "io.element.android.features.location.api"
+}
+
+anvil {
+ generateDaggerFactories.set(true)
+}
+
+dependencies {
+ implementation(libs.dagger)
+ implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.network)
+ implementation(projects.libraries.core)
+ implementation(projects.libraries.uiStrings)
+ implementation(libs.maplibre)
+ implementation(libs.network.retrofit)
+ implementation(libs.maplibre.annotation)
+ implementation(libs.coil.compose)
+ implementation(libs.serialization.json)
+ implementation(libs.accompanist.permission)
+ ksp(libs.showkase.processor)
+
+ testImplementation(libs.test.junit)
+ testImplementation(libs.coroutines.test)
+ testImplementation(libs.molecule.runtime)
+ testImplementation(libs.test.truth)
+ testImplementation(libs.test.turbine)
+ testImplementation(libs.test.truth)
+ testImplementation(projects.libraries.matrix.test)
+}
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt
new file mode 100644
index 0000000000..596bc4d1a0
--- /dev/null
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.location.api
+
+/**
+ * Represents a location sample emitted by the device's location subsystem.
+ */
+data class Location(
+ val lat: Double,
+ val lon: Double,
+ val accuracy: Float,
+)
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/MapView.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/MapView.kt
new file mode 100644
index 0000000000..464422d713
--- /dev/null
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/MapView.kt
@@ -0,0 +1,297 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.location.api
+
+import android.annotation.SuppressLint
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.mapbox.mapboxsdk.Mapbox
+import com.mapbox.mapboxsdk.camera.CameraPosition
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
+import com.mapbox.mapboxsdk.geometry.LatLng
+import com.mapbox.mapboxsdk.maps.MapView
+import com.mapbox.mapboxsdk.maps.MapboxMap
+import com.mapbox.mapboxsdk.maps.Style
+import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager
+import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
+import io.element.android.features.location.api.internal.buildTileServerUrl
+import io.element.android.libraries.designsystem.preview.ElementPreviewDark
+import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.ElementTheme
+import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
+import io.element.android.libraries.designsystem.theme.components.Icon
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+import timber.log.Timber
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+/**
+ * Composable wrapper around MapLibre's [MapView].
+ */
+@SuppressLint("MissingPermission")
+@Composable
+fun MapView(
+ modifier: Modifier = Modifier,
+ mapState: MapState = rememberMapState(),
+ darkMode: Boolean = !ElementTheme.colors.isLight,
+ onLocationClick: () -> Unit,
+) {
+ // When in preview, early return a Box with the received modifier preserving layout
+ if (LocalInspectionMode.current) {
+ @Suppress("ModifierReused") // False positive, the modifier is not reused due to the early return.
+ Box(modifier = modifier)
+ return
+ }
+
+ val context = LocalContext.current
+ val mapView = remember {
+ Mapbox.getInstance(context)
+ MapView(context)
+ }
+ var mapRefs by remember { mutableStateOf(null) }
+
+ // Build map
+ LaunchedEffect(darkMode) {
+ mapView.awaitMap().let { map ->
+ map.uiSettings.apply {
+ isCompassEnabled = false
+ }
+ map.setStyle(buildTileServerUrl(darkMode = darkMode)) { style ->
+ mapRefs = MapRefs(
+ map = map,
+ symbolManager = SymbolManager(mapView, map, style).apply {
+ iconAllowOverlap = true
+ },
+ style = style
+ )
+ }
+ }
+ }
+
+ // Update state position when moving map
+ DisposableEffect(mapRefs) {
+ var listener: MapboxMap.OnCameraIdleListener? = null
+
+ mapRefs?.let { mapRefs ->
+ listener = MapboxMap.OnCameraIdleListener {
+ mapRefs.map.cameraPosition.target?.let { target ->
+ val position = MapState.CameraPosition(
+ lat = target.latitude,
+ lon = target.longitude,
+ zoom = mapRefs.map.cameraPosition.zoom
+ )
+ mapState.position = position
+ Timber.d("Camera moved to: $position")
+ }
+ }.apply {
+ mapRefs.map.addOnCameraIdleListener(this)
+ Timber.d("Added OnCameraIdleListener $this")
+ }
+ }
+
+ onDispose {
+ mapRefs?.let { mapRefs ->
+ listener?.let {
+ mapRefs.map.removeOnCameraIdleListener(it).apply {
+ Timber.d("Removed OnCameraIdleListener $it")
+ }
+ }
+ }
+ }
+ }
+
+ // Move map to given position when state has changed
+ LaunchedEffect(mapRefs, mapState.position) {
+ mapRefs?.map?.moveCamera(
+ CameraUpdateFactory.newCameraPosition(
+ CameraPosition.Builder()
+ .target(LatLng(mapState.position.lat, mapState.position.lon))
+ .zoom(mapState.position.zoom).build()
+ )
+ )
+ Timber.d("Camera position updated to: ${mapState.position}")
+ }
+
+ // Draw pin
+ LaunchedEffect(mapRefs, mapState.location) {
+ mapRefs?.let { mapRefs ->
+ mapState.location?.let { location ->
+ context.getDrawable(R.drawable.pin)?.let { mapRefs.style.addImage("pin", it) }
+ mapRefs.symbolManager.create(
+ SymbolOptions()
+ .withLatLng(LatLng(location.lat, location.lon))
+ .withIconImage("pin")
+ .withIconSize(1.3f)
+ )
+ Timber.d("Shown pin at location: $location")
+ }
+ }
+
+ }
+
+ // Draw markers
+ LaunchedEffect(mapRefs, mapState.markers) {
+ mapRefs?.let { mapRefs ->
+ mapState.markers.forEachIndexed { index, marker ->
+ context.getDrawable(marker.drawable)?.let { mapRefs.style.addImage("marker_$index", it) }
+ mapRefs.symbolManager.create(
+ SymbolOptions()
+ .withLatLng(LatLng(marker.lat, marker.lon))
+ .withIconImage("marker_$index")
+ .withIconSize(1.0f)
+ )
+ Timber.d("Shown marker at location: $marker")
+ }
+ }
+ }
+
+ @Suppress("ModifierReused")
+ Box(modifier = modifier) {
+ AndroidView(factory = { mapView })
+ FloatingActionButton(
+ onClick = onLocationClick,
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(16.dp),
+ ) {
+ Icon(
+ imageVector = Icons.Filled.LocationOn,
+ contentDescription = null, // TODO
+ )
+ }
+ }
+}
+
+@Composable
+fun rememberMapState(
+ position: MapState.CameraPosition = MapState.CameraPosition(lat = 0.0, lon = 0.0, zoom = 0.0),
+ location: Location? = null,
+ markers: ImmutableList = emptyList().toImmutableList(),
+): MapState = remember {
+ MapState(
+ position = position,
+ location = location,
+ markers = markers,
+ )
+} // TODO(Use remember saveable with Parcelable custom saver)
+
+@Stable
+class MapState(
+ position: CameraPosition, // The position of the camera, it's what will be shared
+ location: Location? = null, // The location retrieved by the location subsystem, if any.
+ markers: ImmutableList = emptyList().toImmutableList(), // The pin's location, if any.
+) {
+ var position: CameraPosition by mutableStateOf(position)
+ var location: Location? by mutableStateOf(location)
+ var markers: ImmutableList by mutableStateOf(markers)
+
+ override fun toString(): String {
+ return "MapState(position=$position, location=$location, markers=$markers)"
+ }
+
+ @Stable
+ data class CameraPosition(
+ val lat: Double,
+ val lon: Double,
+ val zoom: Double,
+ )
+
+ @Stable
+ data class Marker(
+ @DrawableRes val drawable: Int,
+ val lat: Double,
+ val lon: Double,
+ )
+}
+
+private class MapRefs(
+ val map: MapboxMap,
+ val symbolManager: SymbolManager,
+ val style: Style
+)
+
+/**
+ * A suspending function that provides an instance of [MapboxMap] from this [MapView]. This is
+ * an alternative to [MapView.getMapAsync] by using coroutines to obtain the [MapboxMap].
+ *
+ * Inspired from [com.google.maps.android.ktx.awaitMap]
+ *
+ * @return the [MapboxMap] instance
+ */
+private suspend inline fun MapView.awaitMap(): MapboxMap =
+ suspendCoroutine { continuation ->
+ getMapAsync {
+ continuation.resume(it)
+ }
+ }
+
+@Preview
+@Composable
+fun MapViewLightPreview() =
+ ElementPreviewLight { ContentToPreview() }
+
+@Preview
+@Composable
+fun MapViewDarkPreview() =
+ ElementPreviewDark { ContentToPreview() }
+
+@Composable
+private fun ContentToPreview() {
+ MapView(
+ modifier = Modifier.size(400.dp),
+ mapState = rememberMapState(
+ position = MapState.CameraPosition(
+ lat = 0.0,
+ lon = 0.0,
+ zoom = 0.0,
+ ),
+ location = Location(
+ lat = 0.0,
+ lon = 0.0,
+ accuracy = 0.0f,
+ ),
+ markers = listOf(
+ MapState.Marker(
+ drawable = R.drawable.pin,
+ lat = 0.0,
+ lon = 0.0,
+ )
+ ).toImmutableList()
+ ),
+ onLocationClick = {},
+ )
+}
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt
new file mode 100644
index 0000000000..e68c42368f
--- /dev/null
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.location.api
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImagePainter
+import coil.compose.rememberAsyncImagePainter
+import coil.request.ImageRequest
+import io.element.android.libraries.designsystem.preview.ElementPreviewDark
+import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.ElementTheme
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.features.location.api.internal.StaticMapPlaceholder
+import io.element.android.features.location.api.internal.buildStaticMapsApiUrl
+import timber.log.Timber
+
+/**
+ * Shows a static map image downloaded via a third party service's static maps API.
+ */
+@Composable
+fun StaticMapView(
+ lat: Double,
+ lon: Double,
+ zoom: Double,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ darkMode: Boolean = !ElementTheme.colors.isLight,
+) {
+ // Using BoxWithConstraints to:
+ // 1) Size the inner Image to the same Dp size of the outer BoxWithConstraints.
+ // 2) Request the static map image of the exact required size in Px to fill the AsyncImage.
+ BoxWithConstraints(
+ modifier = modifier,
+ contentAlignment = Alignment.Center
+ ) {
+ var retryHash by remember { mutableStateOf(0) }
+ val painter = rememberAsyncImagePainter(
+ model = if (constraints.isZero) {
+ // Avoid building a URL if any of the size constraints is zero (else it will thrown an exception).
+ null
+ } else {
+ ImageRequest.Builder(LocalContext.current)
+ .data(
+ buildStaticMapsApiUrl(
+ lat = lat,
+ lon = lon,
+ desiredZoom = zoom,
+ desiredWidth = constraints.maxWidth,
+ desiredHeight = constraints.maxHeight,
+ darkMode = darkMode,
+ )
+ )
+ .size(width = constraints.maxWidth, height = constraints.maxHeight)
+ .setParameter("retry_hash", retryHash, memoryCacheKey = null)
+ .build()
+ }.apply {
+ Timber.d("Static map image request: ${this?.data}")
+ }
+ )
+
+ if (painter.state is AsyncImagePainter.State.Success) {
+ Image(
+ painter = painter,
+ contentDescription = contentDescription,
+ modifier = Modifier.size(width = maxWidth, height = maxHeight),
+ // The returned image can be smaller than the requested size due to the static maps API having
+ // a max width and height of 2048 px. See buildStaticMapsApiUrl() for more details.
+ // We apply ContentScale.Fit to scale the image to fill the AsyncImage should this be the case.
+ contentScale = ContentScale.Fit,
+ )
+ Icon(
+ resourceId = R.drawable.pin,
+ contentDescription = null,
+ tint = Color.Unspecified
+ )
+ } else {
+ StaticMapPlaceholder(
+ showProgress = painter.state is AsyncImagePainter.State.Loading,
+ contentDescription = contentDescription,
+ modifier = Modifier.size(width = maxWidth, height = maxHeight),
+ darkMode = darkMode,
+ onLoadMapClick = { retryHash++ }
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun StaticMapViewLightPreview() =
+ ElementPreviewLight { ContentToPreview() }
+
+@Preview
+@Composable
+fun StaticMapViewDarkPreview() =
+ ElementPreviewDark { ContentToPreview() }
+
+@Composable
+private fun ContentToPreview() {
+ StaticMapView(
+ lat = 0.0,
+ lon = 0.0,
+ zoom = 0.0,
+ contentDescription = null,
+ modifier = Modifier.size(400.dp),
+ )
+}
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapsUtils.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapsUtils.kt
new file mode 100644
index 0000000000..f5a15e46c6
--- /dev/null
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapsUtils.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.location.api.internal
+
+import kotlin.math.roundToInt
+
+private const val API_KEY = "fU3vlMsMn4Jb6dnEIFsx"
+private const val BASE_URL = "https://api.maptiler.com"
+private const val LIGHT_MAP_ID = "9bc819c8-e627-474a-a348-ec144fe3d810"
+private const val DARK_MAP_ID = "dea61faf-292b-4774-9660-58fcef89a7f3"
+private const val STATIC_MAP_FORMAT = "webp"
+private const val STATIC_MAP_SCALE = "" // Either "" (empty string) for normal image or "@2x" for retina images.
+private const val STATIC_MAP_MAX_WIDTH_HEIGHT = 2048
+private const val STATIC_MAP_MAX_ZOOM = 22.0
+
+internal fun buildTileServerUrl(
+ darkMode: Boolean
+): String = if (!darkMode) {
+ "$BASE_URL/maps/$LIGHT_MAP_ID/style.json?key=$API_KEY"
+} else {
+ "$BASE_URL/maps/$DARK_MAP_ID/style.json?key=$API_KEY"
+}
+
+/**
+ * Builds a valid URL for maptiler.com static map api based on the given params.
+ *
+ * Coerces width and height to the API maximum of 2048 keeping the requested aspect ratio.
+ * Coerces zoom to the API maximum of 22.
+ *
+ * NB: This will throw if either width or height are <= 0. You need to handle this case upstream
+ * (hint: views can't have negative width or height but can have 0 width or height sometimes).
+ */
+internal fun buildStaticMapsApiUrl(
+ lat: Double,
+ lon: Double,
+ desiredZoom: Double,
+ desiredWidth: Int,
+ desiredHeight: Int,
+ darkMode: Boolean
+): String {
+ require(desiredWidth > 0 && desiredHeight > 0) {
+ "Width ($desiredHeight) and height ($desiredHeight) must be > 0"
+ }
+ require(desiredZoom >= 0) { "Zoom ($desiredZoom) must be >= 0" }
+ val zoom = desiredZoom.coerceAtMost(STATIC_MAP_MAX_ZOOM) // API will error if outside 0-22 range.
+ val width: Int
+ val height: Int
+ if (desiredWidth <= STATIC_MAP_MAX_WIDTH_HEIGHT && desiredHeight <= STATIC_MAP_MAX_WIDTH_HEIGHT) {
+ width = desiredWidth
+ height = desiredHeight
+ } else {
+ val aspectRatio = desiredWidth.toDouble() / desiredHeight.toDouble()
+ if (desiredWidth >= desiredHeight) {
+ width = desiredWidth.coerceAtMost(STATIC_MAP_MAX_WIDTH_HEIGHT)
+ height = (width / aspectRatio).roundToInt()
+ } else {
+ height = desiredHeight.coerceAtMost(STATIC_MAP_MAX_WIDTH_HEIGHT)
+ width = (height * aspectRatio).roundToInt()
+ }
+ }
+ return if (!darkMode) {
+ "$BASE_URL/maps/$LIGHT_MAP_ID/static/${lon},${lat},${zoom}/${width}x${height}$STATIC_MAP_SCALE.$STATIC_MAP_FORMAT?key=$API_KEY"
+ } else {
+ "$BASE_URL/maps/$DARK_MAP_ID/static/${lon},${lat},${zoom}/${width}x${height}$STATIC_MAP_SCALE.$STATIC_MAP_FORMAT?key=$API_KEY"
+ }
+}
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt
new file mode 100644
index 0000000000..d39bfd7d15
--- /dev/null
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.location.api.internal
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.preview.ElementPreviewDark
+import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.ElementTheme
+import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.features.location.api.R
+import io.element.android.libraries.ui.strings.R as StringsR
+
+@Composable
+internal fun StaticMapPlaceholder(
+ showProgress: Boolean,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ darkMode: Boolean = !ElementTheme.colors.isLight,
+ onLoadMapClick: () -> Unit,
+) {
+ Box(
+ contentAlignment = Alignment.Center,
+ ) {
+ Image(
+ painter = painterResource(
+ id = if (darkMode) R.drawable.blurred_map_dark
+ else R.drawable.blurred_map_light
+ ),
+ contentDescription = contentDescription,
+ modifier = modifier,
+ contentScale = ContentScale.FillBounds,
+ )
+ if (showProgress) {
+ CircularProgressIndicator()
+ } else {
+ Box(
+ modifier = modifier.clickable(onClick = onLoadMapClick),
+ contentAlignment = Alignment.Center,
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Icon(
+ imageVector = Icons.Default.Refresh,
+ contentDescription = null
+ )
+ Text(text = stringResource(id = StringsR.string.action_static_map_load))
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun StaticMapPlaceholderLightPreview(
+ @PreviewParameter(BooleanParameterProvider::class) values: Boolean
+) = ElementPreviewLight { ContentToPreview(values) }
+
+@Preview
+@Composable
+fun StaticMapPlaceholderDarkPreview(
+ @PreviewParameter(BooleanParameterProvider::class) values: Boolean
+) = ElementPreviewDark { ContentToPreview(values) }
+
+@Composable
+private fun ContentToPreview(showProgress: Boolean) {
+ StaticMapPlaceholder(
+ showProgress = showProgress,
+ contentDescription = null,
+ modifier = Modifier.size(400.dp),
+ onLoadMapClick = {},
+ )
+}
+
+internal class BooleanParameterProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(true, false)
+}
diff --git a/features/location/api/src/main/res/drawable/blurred_map_dark.png b/features/location/api/src/main/res/drawable/blurred_map_dark.png
new file mode 100644
index 0000000000..7e90d568f1
Binary files /dev/null and b/features/location/api/src/main/res/drawable/blurred_map_dark.png differ
diff --git a/features/location/api/src/main/res/drawable/blurred_map_light.png b/features/location/api/src/main/res/drawable/blurred_map_light.png
new file mode 100644
index 0000000000..365cf96786
Binary files /dev/null and b/features/location/api/src/main/res/drawable/blurred_map_light.png differ
diff --git a/features/location/api/src/main/res/drawable/pin.xml b/features/location/api/src/main/res/drawable/pin.xml
new file mode 100644
index 0000000000..9f47b9024f
--- /dev/null
+++ b/features/location/api/src/main/res/drawable/pin.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/BuildStaticMapsApiUrlTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/BuildStaticMapsApiUrlTest.kt
new file mode 100644
index 0000000000..023c7be365
--- /dev/null
+++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/BuildStaticMapsApiUrlTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.location.api.internal
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.location.api.internal.buildStaticMapsApiUrl
+import org.junit.Test
+
+class BuildStaticMapsApiUrlTest {
+ @Test
+ fun `buildStaticMapsApiUrl builds light mode url`() {
+ assertThat(
+ buildStaticMapsApiUrl(
+ lat = 1.234,
+ lon = 5.678,
+ desiredZoom = 1.2,
+ desiredWidth = 100,
+ desiredHeight = 200,
+ darkMode = false
+ )
+ ).isEqualTo(
+ "https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,1.2/100x200.webp?key=fU3vlMsMn4Jb6dnEIFsx"
+ )
+ }
+
+ @Test
+ fun `buildStaticMapsApiUrl builds dark mode url`() {
+ assertThat(
+ buildStaticMapsApiUrl(
+ lat = 1.234,
+ lon = 5.678,
+ desiredZoom = 1.2,
+ desiredWidth = 100,
+ desiredHeight = 200,
+ darkMode = true
+ )
+ ).isEqualTo(
+ "https://api.maptiler.com/maps/dea61faf-292b-4774-9660-58fcef89a7f3/static/5.678,1.234,1.2/100x200.webp?key=fU3vlMsMn4Jb6dnEIFsx"
+ )
+ }
+
+ @Test
+ fun `buildStaticMapsApiUrl coerces zoom at 22 and width and height at max 2048 keeping aspect ratio`() {
+ assertThat(
+ buildStaticMapsApiUrl(
+ lat = 1.234,
+ lon = 5.678,
+ desiredZoom = 100.0,
+ desiredWidth = 8192,
+ desiredHeight = 4096,
+ darkMode = false
+ )
+ ).isEqualTo(
+ "https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,22.0/2048x1024.webp?key=fU3vlMsMn4Jb6dnEIFsx"
+ )
+ }
+}
diff --git a/features/location/fake/build.gradle.kts b/features/location/fake/build.gradle.kts
new file mode 100644
index 0000000000..cceab3f2b7
--- /dev/null
+++ b/features/location/fake/build.gradle.kts
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("io.element.android-compose-library")
+ alias(libs.plugins.anvil)
+ alias(libs.plugins.kotlin.serialization)
+}
+
+android {
+ namespace = "io.element.android.features.location.fake"
+}
+
+anvil {
+ generateDaggerFactories.set(true)
+}
+
+dependencies {
+ implementation(libs.dagger)
+ api(projects.features.location.api)
+ implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.di)
+ implementation(projects.libraries.network)
+ implementation(projects.libraries.core)
+ implementation(libs.maplibre)
+ implementation(libs.network.retrofit)
+ implementation(libs.maplibre.annotation)
+ implementation(libs.coil.compose)
+ implementation(libs.serialization.json)
+ implementation(libs.accompanist.permission)
+
+ testImplementation(libs.test.junit)
+ testImplementation(libs.coroutines.test)
+ testImplementation(libs.molecule.runtime)
+ testImplementation(libs.test.truth)
+ testImplementation(libs.test.turbine)
+ testImplementation(libs.test.truth)
+ testImplementation(projects.libraries.matrix.test)
+}
diff --git a/features/location/fake/src/main/kotlin/io/element/android/features/location/fake/LocationUpdatesFlowFake.kt b/features/location/fake/src/main/kotlin/io/element/android/features/location/fake/LocationUpdatesFlowFake.kt
new file mode 100644
index 0000000000..c3f070acbb
--- /dev/null
+++ b/features/location/fake/src/main/kotlin/io/element/android/features/location/fake/LocationUpdatesFlowFake.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.location.fake
+
+import io.element.android.features.location.api.Location
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+fun fakeLocationUpdatesFlow(): Flow = flow {
+ while (true) {
+ delay(1_000)
+ emit(aLocation())
+ }
+}
+
+private fun aLocation() = Location(
+ lat = 51.49404,
+ lon = -0.25484,
+ accuracy = 5f
+)
diff --git a/features/location/impl/build.gradle.kts b/features/location/impl/build.gradle.kts
new file mode 100644
index 0000000000..66d29fd6bb
--- /dev/null
+++ b/features/location/impl/build.gradle.kts
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("io.element.android-compose-library")
+ alias(libs.plugins.anvil)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.ksp)
+}
+
+android {
+ namespace = "io.element.android.features.location.impl"
+}
+
+anvil {
+ generateDaggerFactories.set(true)
+}
+
+dependencies {
+ implementation(libs.dagger)
+ api(projects.features.location.api)
+ implementation(projects.libraries.di)
+ implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.network)
+ implementation(projects.libraries.core)
+ implementation(libs.maplibre)
+ implementation(libs.network.retrofit)
+ implementation(libs.maplibre.annotation)
+ implementation(libs.coil.compose)
+ implementation(libs.serialization.json)
+ implementation(libs.accompanist.permission)
+ ksp(libs.showkase.processor)
+
+ testImplementation(libs.test.junit)
+ testImplementation(libs.coroutines.test)
+ testImplementation(libs.molecule.runtime)
+ testImplementation(libs.test.truth)
+ testImplementation(libs.test.turbine)
+ testImplementation(libs.test.truth)
+ testImplementation(projects.libraries.matrix.test)
+}
diff --git a/features/location/impl/src/main/AndroidManifest.xml b/features/location/impl/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..b4f5d8f271
--- /dev/null
+++ b/features/location/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/LocationUpdatesFlowImpl.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/LocationUpdatesFlowImpl.kt
new file mode 100644
index 0000000000..11b1e2a02d
--- /dev/null
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/LocationUpdatesFlowImpl.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.location.impl
+
+import android.Manifest
+import android.content.Context
+import android.location.LocationManager
+import androidx.annotation.RequiresPermission
+import androidx.core.content.getSystemService
+import androidx.core.location.LocationListenerCompat
+import androidx.core.location.LocationManagerCompat
+import androidx.core.location.LocationRequestCompat
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.features.location.api.Location
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.channels.trySendBlocking
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+
+/**
+ * Returns a cold [Flow] that, once collected, emits [Location] updates every second.
+ */
+@RequiresPermission(
+ anyOf = [
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ]
+)
+fun locationUpdatesFlow(
+ context: Context,
+ coroutineDispatchers: CoroutineDispatchers,
+): Flow = callbackFlow {
+ val locationManager: LocationManager = checkNotNull(context.getSystemService())
+ val provider = locationManager.bestAvailableProvider()
+ // Try to eagerly emit the last known location as fast as possible
+ locationManager.getLastKnownLocation(provider)?.let { location ->
+ trySendBlocking(
+ Location(
+ lat = location.latitude,
+ lon = location.longitude,
+ accuracy = location.accuracy
+ )
+ )
+ }
+ val locationListener = LocationListenerCompat { location ->
+ trySendBlocking(
+ Location(
+ lat = location.latitude,
+ lon = location.longitude,
+ accuracy = location.accuracy
+ )
+ )
+ }
+ LocationManagerCompat.requestLocationUpdates(
+ locationManager,
+ provider,
+ buildLocationRequest(),
+ coroutineDispatchers.io.asExecutor(),
+ locationListener,
+ )
+ awaitClose {
+ LocationManagerCompat.removeUpdates(locationManager, locationListener)
+ }
+}
+
+private fun LocationManager.bestAvailableProvider(): String =
+ checkNotNull(getProviders(true).maxByOrNull { providerPriority(it) }) {
+ "No location provider available"
+ }
+
+private fun providerPriority(provider: String): Int = when (provider) {
+ LocationManager.FUSED_PROVIDER -> 4
+ LocationManager.GPS_PROVIDER -> 3
+ LocationManager.NETWORK_PROVIDER -> 2
+ LocationManager.PASSIVE_PROVIDER -> 1
+ else -> 0
+}
+
+private fun buildLocationRequest() = LocationRequestCompat.Builder(1_000).apply {
+ setMinUpdateIntervalMillis(1_000)
+}.build()
diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml
index d1393dbaf3..ef7197ac96 100644
--- a/features/login/impl/src/main/res/values-de/translations.xml
+++ b/features/login/impl/src/main/res/values-de/translations.xml
@@ -13,7 +13,7 @@
"Andere"
"Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Arbeitskonto."
"Kontoanbieter ändern"
- "Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfen Sie, ob Sie die Homeserver-URL korrekt eingegeben haben. Wenn die URL korrekt ist, wenden Sie sich an Ihren Homeserver-Administrator, um weitere Hilfe zu erhalten."
+ "Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfe, dass du die Homeserver-URL korrekt eingegeben hast. Wenn die URL korrekt ist, wende dich an deinen Homeserver-Administrator für weitere Hilfe."
"Dieser Server unterstützt derzeit keine Sliding Sync."
"Homeserver-URL"
"Du kannst dich nur mit einem existierenden Server verbinden, der Sliding Sync unterstützt. Dein Homeserver-Administrator muss es konfigurieren. %1$s"
diff --git a/features/login/impl/src/main/res/values-sk/translations.xml b/features/login/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..6e1ef23a72
--- /dev/null
+++ b/features/login/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,23 @@
+
+
+ "Pokračovať"
+ "Adresa domovského servera"
+ "Adresa URL domovského servera"
+ "Aká je adresa vášho servera?"
+ "Tento účet bol deaktivovaný."
+ "Nesprávne používateľské meno a/alebo heslo"
+ "Zadajte svoje údaje"
+ "Kde žijú vaše rozhovory"
+ "Vitajte späť!"
+ "Prihlásiť sa do %1$s"
+ "Zmeniť poskytovateľa účtu"
+ "Súkromný server pre zamestnancov spoločnosti Element."
+ "Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."
+ "Chystáte sa prihlásiť do %1$s"
+ "Chystáte sa vytvoriť účet na %1$s"
+ "Pokračovať"
+ "Vyberte svoj server"
+ "Heslo"
+ "Pokračovať"
+ "Používateľské meno"
+
diff --git a/features/logout/api/src/main/res/values-sk/translations.xml b/features/logout/api/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..212f11ccbc
--- /dev/null
+++ b/features/logout/api/src/main/res/values-sk/translations.xml
@@ -0,0 +1,8 @@
+
+
+ "Ste si istí, že sa chcete odhlásiť?"
+ "Odhlásiť sa"
+ "Prebieha odhlasovanie…"
+ "Odhlásiť sa"
+ "Odhlásiť sa"
+
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
index 1bc0be0626..607d4df85f 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
@@ -58,7 +58,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.SnackbarMessage
-import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
+import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
@@ -110,7 +110,7 @@ class MessagesPresenter @AssistedInject constructor(
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
- val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
+ val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
LaunchedEffect(syncUpdateFlow) {
roomAvatar.value =
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt
index 7500f0d91e..ff2f8aaeeb 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt
@@ -21,7 +21,7 @@ import android.net.Uri
import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor
-import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter
+import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.androidutils.file.getFileName
import io.element.android.libraries.androidutils.file.getFileSize
import io.element.android.libraries.androidutils.file.getMimeType
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt
index 88bf7c91c2..5ef1bdcd47 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt
@@ -35,7 +35,7 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.SnackbarMessage
-import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
+import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile
import kotlinx.coroutines.CoroutineScope
@@ -66,7 +66,7 @@ class MediaViewerPresenter @AssistedInject constructor(
val localMedia: MutableState> = remember {
mutableStateOf(Async.Uninitialized)
}
- val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
+ val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
localMediaActions.Configure()
DisposableEffect(loadMediaTrigger) {
coroutineScope.downloadMedia(mediaFile, localMedia)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt
index c99ab46ab1..d9a12cf615 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt
@@ -25,8 +25,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor
-import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter
import io.element.android.features.messages.impl.timeline.util.toHtmlDocument
+import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml
index 7300efe1d6..8da8200bd5 100644
--- a/features/messages/impl/src/main/res/values-cs/translations.xml
+++ b/features/messages/impl/src/main/res/values-cs/translations.xml
@@ -10,10 +10,12 @@
"Natočit video"
"Příloha"
"Knihovna fotografií a videí"
+ "Poloha"
"Nepodařilo se načíst údaje o uživateli"
"Chtěli byste je pozvat zpět?"
"V tomto chatu jste sami"
- "Nemáte oprávnění vkládat příspěvky do této místnosti"
+ "Zpráva zkopírována"
+ "Nemáte oprávnění zveřejňovat příspěvky v této místnosti"
"Odeslat znovu"
"Vaši zprávu se nepodařilo odeslat"
"Nahrání média se nezdařilo, zkuste to prosím znovu."
diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..68b412f979
--- /dev/null
+++ b/features/messages/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,16 @@
+
+
+
+ - "%1$d zmena miestnosti"
+ - "%1$d zmeny miestnosti"
+ - "%1$d zmien miestnosti"
+
+ "Kamera"
+ "Odfotiť"
+ "Nahrať video"
+ "Príloha"
+ "Knižnica fotografií a videí"
+ "Poloha"
+ "Odoslať znova"
+ "Odstrániť"
+
diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml
index 508acf0e33..79d70cd4b7 100644
--- a/features/messages/impl/src/main/res/values/localazy.xml
+++ b/features/messages/impl/src/main/res/values/localazy.xml
@@ -9,6 +9,7 @@
"Record a video"
"Attachment"
"Photo & Video Library"
+ "Location"
"Could not retrieve user details"
"Would you like to invite them back?"
"You are alone in this chat"
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
index beb37f8e51..41daff47fd 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
@@ -32,7 +32,7 @@ import io.element.android.features.messages.impl.timeline.factories.virtual.Time
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractorWithoutValidation
-import io.element.android.features.messages.timeline.FakeFileSizeFormatter
+import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
diff --git a/features/onboarding/impl/src/main/res/values-cs/translations.xml b/features/onboarding/impl/src/main/res/values-cs/translations.xml
index 176c446673..6b8f0eaa91 100644
--- a/features/onboarding/impl/src/main/res/values-cs/translations.xml
+++ b/features/onboarding/impl/src/main/res/values-cs/translations.xml
@@ -4,6 +4,7 @@
"Přihlásit se pomocí QR kódu"
"Vytvořit účet"
"Komunikujte a spolupracujte bezpečně"
+ "Vítejte u dosud nejrychlejšího Elementu. Vylepšený pro rychlost a jednoduchost."
"Vítejte v %1$s. Vylepšený, pro rychlost a jednoduchost."
"Buďte ve svém živlu"
diff --git a/features/onboarding/impl/src/main/res/values-sk/translations.xml b/features/onboarding/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..9e4e162770
--- /dev/null
+++ b/features/onboarding/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Vytvoriť účet"
+
diff --git a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/CacheService.kt b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/CacheService.kt
new file mode 100644
index 0000000000..0bc9285853
--- /dev/null
+++ b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/CacheService.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.preferences.api
+
+import kotlinx.coroutines.flow.Flow
+
+interface CacheService {
+ /**
+ * Returns a flow of the current cache index, can let the app to know when the
+ * cache has been cleared, for instance to restart the app.
+ * Will be a flow of Int, starting from 0, and incrementing each time the cache is cleared.
+ */
+ fun cacheIndex(): Flow
+}
diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts
index 631884fe47..1e76ee5c93 100644
--- a/features/preferences/impl/build.gradle.kts
+++ b/features/preferences/impl/build.gradle.kts
@@ -32,6 +32,7 @@ anvil {
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
+ implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
@@ -39,6 +40,7 @@ dependencies {
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.featureflag.ui)
implementation(projects.libraries.elementresources)
+ implementation(projects.libraries.network)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.features.rageshake.api)
@@ -47,6 +49,7 @@ dependencies {
implementation(projects.features.logout.api)
implementation(libs.datetime)
implementation(libs.accompanist.placeholder)
+ implementation(libs.coil.compose)
api(projects.features.preferences.api)
ksp(libs.showkase.processor)
@@ -62,6 +65,7 @@ dependencies {
testImplementation(projects.features.logout.impl)
testImplementation(projects.features.analytics.test)
testImplementation(projects.features.analytics.impl)
+ testImplementation(projects.tests.testutils)
androidTestImplementation(libs.test.junitext)
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/FileSizeFormatter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultCacheService.kt
similarity index 56%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/FileSizeFormatter.kt
rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultCacheService.kt
index 4ede9b7f21..7675ec3dd6 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/FileSizeFormatter.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultCacheService.kt
@@ -14,25 +14,26 @@
* limitations under the License.
*/
-package io.element.android.features.messages.impl.timeline.util
+package io.element.android.features.preferences.impl
-import android.content.Context
-import android.text.format.Formatter
import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.preferences.api.CacheService
import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.di.SingleIn
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
-interface FileSizeFormatter {
- /**
- * Formats a content size to be in the form of bytes, kilobytes, megabytes, etc.
- */
- fun format(fileSize: Long): String
-}
-
+@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
-class AndroidFileSizeFormatter @Inject constructor(@ApplicationContext private val context: Context) : FileSizeFormatter {
- override fun format(fileSize: Long): String {
- return Formatter.formatShortFileSize(context, fileSize)
+class DefaultCacheService @Inject constructor() : CacheService {
+ private val cacheIndexState = MutableStateFlow(0)
+
+ override fun cacheIndex(): Flow {
+ return cacheIndexState
+ }
+
+ fun incrementCacheIndex() {
+ cacheIndexState.value++
}
}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt
index b79484592f..bb3879b129 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt
@@ -20,4 +20,5 @@ import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
sealed interface DeveloperSettingsEvents {
data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents
+ object ClearCache: DeveloperSettingsEvents
}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
index 9f9cd636eb..d4430dd2e3 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
@@ -18,12 +18,18 @@ package io.element.android.features.preferences.impl.developer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshots.SnapshotStateMap
+import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
+import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
+import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.architecture.execute
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.featureflag.api.Feature
import io.element.android.libraries.featureflag.api.FeatureFlagService
@@ -36,6 +42,8 @@ import javax.inject.Inject
class DeveloperSettingsPresenter @Inject constructor(
private val featureFlagService: FeatureFlagService,
+ private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
+ private val clearCacheUseCase: ClearCacheUseCase,
) : Presenter {
@Composable
@@ -47,6 +55,12 @@ class DeveloperSettingsPresenter @Inject constructor(
val enabledFeatures = remember {
mutableStateMapOf()
}
+ val cacheSize = remember {
+ mutableStateOf>(Async.Uninitialized)
+ }
+ val clearCacheAction = remember {
+ mutableStateOf>(Async.Uninitialized)
+ }
LaunchedEffect(Unit) {
FeatureFlags.values().forEach { feature ->
features[feature.key] = feature
@@ -55,6 +69,10 @@ class DeveloperSettingsPresenter @Inject constructor(
}
val featureUiModels = createUiModels(features, enabledFeatures)
val coroutineScope = rememberCoroutineScope()
+ // Compute cache size each time the clear cache action value is changed
+ LaunchedEffect(clearCacheAction.value) {
+ computeCacheSize(cacheSize)
+ }
fun handleEvents(event: DeveloperSettingsEvents) {
when (event) {
@@ -64,11 +82,14 @@ class DeveloperSettingsPresenter @Inject constructor(
event.feature,
event.isEnabled
)
+ DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
}
}
return DeveloperSettingsState(
features = featureUiModels.toImmutableList(),
+ cacheSize = cacheSize.value,
+ clearCacheAction = clearCacheAction.value,
eventSink = ::handleEvents
)
}
@@ -103,6 +124,18 @@ class DeveloperSettingsPresenter @Inject constructor(
enabledFeatures[featureUiModel.key] = enabled
}
}
+
+ private fun CoroutineScope.computeCacheSize(cacheSize: MutableState>) = launch {
+ suspend {
+ computeCacheSizeUseCase()
+ }.execute(cacheSize)
+ }
+
+ private fun CoroutineScope.clearCache(clearCacheAction: MutableState>) = launch {
+ suspend {
+ clearCacheUseCase()
+ }.execute(clearCacheAction)
+ }
}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt
index 53ff80967e..61205e7f7d 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt
@@ -16,10 +16,13 @@
package io.element.android.features.preferences.impl.developer
+import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import kotlinx.collections.immutable.ImmutableList
-data class DeveloperSettingsState(
+data class DeveloperSettingsState constructor(
val features: ImmutableList,
+ val cacheSize: Async,
+ val clearCacheAction: Async,
val eventSink: (DeveloperSettingsEvents) -> Unit
)
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt
index f69f73e6e5..de94bd6664 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt
@@ -17,16 +17,20 @@
package io.element.android.features.preferences.impl.developer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList
open class DeveloperSettingsStateProvider : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aDeveloperSettingsState(),
+ aDeveloperSettingsState().copy(clearCacheAction = Async.Loading()),
)
}
fun aDeveloperSettingsState() = DeveloperSettingsState(
features = aFeatureUiModelList(),
+ cacheSize = Async.Success("1.2 MB"),
+ clearCacheAction = Async.Uninitialized,
eventSink = {}
)
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt
index 697081c397..027b3cfd1d 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt
@@ -16,11 +16,15 @@
package io.element.android.features.preferences.impl.developer
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.architecture.isLoading
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
@@ -52,6 +56,20 @@ fun DeveloperSettingsView(
onClick = onOpenShowkase
)
}
+ val cache = state.cacheSize
+ PreferenceCategory(title = "Cache") {
+ PreferenceText(
+ title = "Clear cache",
+ icon = Icons.Default.Delete,
+ currentValue = cache.dataOrNull(),
+ loadingCurrentValue = state.cacheSize.isLoading() || state.clearCacheAction.isLoading(),
+ onClick = {
+ if (state.clearCacheAction.isLoading().not()) {
+ state.eventSink(DeveloperSettingsEvents.ClearCache)
+ }
+ }
+ )
+ }
}
}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt
new file mode 100644
index 0000000000..f7b0d01130
--- /dev/null
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoilApi::class)
+
+package io.element.android.features.preferences.impl.tasks
+
+import android.content.Context
+import coil.Coil
+import coil.annotation.ExperimentalCoilApi
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.preferences.impl.DefaultCacheService
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.MatrixClient
+import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+import javax.inject.Inject
+import javax.inject.Provider
+
+interface ClearCacheUseCase {
+ suspend operator fun invoke()
+}
+
+@ContributesBinding(SessionScope::class)
+class DefaultClearCacheUseCase @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val matrixClient: MatrixClient,
+ private val coroutineDispatchers: CoroutineDispatchers,
+ private val defaultCacheIndexProvider: DefaultCacheService,
+ private val okHttpClient: Provider,
+) : ClearCacheUseCase {
+ override suspend fun invoke() = withContext(coroutineDispatchers.io) {
+ // Clear Matrix cache
+ matrixClient.clearCache()
+ // Clear Coil cache
+ Coil.imageLoader(context).let {
+ it.diskCache?.clear()
+ it.memoryCache?.clear()
+ }
+ // Clear OkHttp cache
+ okHttpClient.get().cache?.delete()
+ // Clear app cache
+ context.cacheDir.deleteRecursively()
+ // Ensure the app is restarted
+ defaultCacheIndexProvider.incrementCacheIndex()
+ }
+}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt
new file mode 100644
index 0000000000..661f6493ec
--- /dev/null
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.preferences.impl.tasks
+
+import android.content.Context
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
+import io.element.android.libraries.androidutils.file.getSizeOfFiles
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.MatrixClient
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+interface ComputeCacheSizeUseCase {
+ suspend operator fun invoke(): String
+}
+
+@ContributesBinding(SessionScope::class)
+class DefaultComputeCacheSizeUseCase @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val matrixClient: MatrixClient,
+ private val coroutineDispatchers: CoroutineDispatchers,
+ private val fileSizeFormatter: FileSizeFormatter,
+) : ComputeCacheSizeUseCase {
+ override suspend fun invoke(): String = withContext(coroutineDispatchers.io) {
+ var cumulativeSize = 0L
+ cumulativeSize += matrixClient.getCacheSize()
+ // - 4096 to not include the size fo the folder
+ cumulativeSize += (context.cacheDir.getSizeOfFiles() - 4096).coerceAtLeast(0)
+ fileSizeFormatter.format(cumulativeSize)
+ }
+}
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt
index 6b7c8c2df4..226140647d 100644
--- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt
@@ -20,6 +20,9 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
+import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
+import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import kotlinx.coroutines.test.runTest
@@ -29,13 +32,17 @@ class DeveloperSettingsPresenterTest {
@Test
fun `present - ensures initial state is correct`() = runTest {
val presenter = DeveloperSettingsPresenter(
- FakeFeatureFlagService()
+ FakeFeatureFlagService(),
+ FakeComputeCacheSizeUseCase(),
+ FakeClearCacheUseCase(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.features).isEmpty()
+ assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized)
+ assertThat(initialState.cacheSize).isEqualTo(Async.Uninitialized)
cancelAndIgnoreRemainingEvents()
}
}
@@ -43,7 +50,9 @@ class DeveloperSettingsPresenterTest {
@Test
fun `present - ensures feature list is loaded`() = runTest {
val presenter = DeveloperSettingsPresenter(
- FakeFeatureFlagService()
+ FakeFeatureFlagService(),
+ FakeComputeCacheSizeUseCase(),
+ FakeClearCacheUseCase(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -58,7 +67,9 @@ class DeveloperSettingsPresenterTest {
@Test
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
val presenter = DeveloperSettingsPresenter(
- FakeFeatureFlagService()
+ FakeFeatureFlagService(),
+ FakeComputeCacheSizeUseCase(),
+ FakeClearCacheUseCase(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -74,4 +85,28 @@ class DeveloperSettingsPresenterTest {
cancelAndIgnoreRemainingEvents()
}
}
+
+ @Test
+ fun `present - clear cache`() = runTest {
+ val clearCacheUseCase = FakeClearCacheUseCase()
+ val presenter = DeveloperSettingsPresenter(
+ FakeFeatureFlagService(),
+ FakeComputeCacheSizeUseCase(),
+ clearCacheUseCase,
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ skipItems(1)
+ val initialState = awaitItem()
+ assertThat(clearCacheUseCase.executeHasBeenCalled).isFalse()
+ initialState.eventSink(DeveloperSettingsEvents.ClearCache)
+ val stateAfterEvent = awaitItem()
+ assertThat(stateAfterEvent.clearCacheAction).isInstanceOf(Async.Loading::class.java)
+ skipItems(1)
+ assertThat(awaitItem().clearCacheAction).isInstanceOf(Async.Success::class.java)
+ assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
}
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/FakeClearCacheUseCase.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/FakeClearCacheUseCase.kt
new file mode 100644
index 0000000000..7415e09e96
--- /dev/null
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/FakeClearCacheUseCase.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.preferences.impl.tasks
+
+import io.element.android.tests.testutils.simulateLongTask
+
+class FakeClearCacheUseCase : ClearCacheUseCase {
+ var executeHasBeenCalled = false
+ private set
+
+ override suspend fun invoke() = simulateLongTask {
+ executeHasBeenCalled = true
+ }
+}
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/FakeComputeCacheSizeUseCase.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/FakeComputeCacheSizeUseCase.kt
new file mode 100644
index 0000000000..fa8556630f
--- /dev/null
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/FakeComputeCacheSizeUseCase.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.preferences.impl.tasks
+
+import io.element.android.tests.testutils.simulateLongTask
+
+class FakeComputeCacheSizeUseCase : ComputeCacheSizeUseCase {
+ override suspend fun invoke() = simulateLongTask {
+ "O kB"
+ }
+}
diff --git a/features/rageshake/api/src/main/res/values-sk/translations.xml b/features/rageshake/api/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..fb9ccf4168
--- /dev/null
+++ b/features/rageshake/api/src/main/res/values-sk/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Zdá sa, že zúrivo trasiete telefónom. Chcete otvoriť obrazovku s nahlásením chýb?"
+
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
index 874da06acb..91f761bda9 100755
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
@@ -52,6 +52,7 @@ import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.util.Locale
import javax.inject.Inject
+import javax.inject.Provider
/**
* BugReporter creates and sends the bug reports.
@@ -62,7 +63,7 @@ class DefaultBugReporter @Inject constructor(
private val screenshotHolder: ScreenshotHolder,
private val crashDataStore: CrashDataStore,
private val coroutineDispatchers: CoroutineDispatchers,
- private val okHttpClient: OkHttpClient,
+ private val okHttpClient: Provider,
/*
private val activeSessionHolder: ActiveSessionHolder,
private val versionProvider: VersionProvider,
@@ -339,7 +340,7 @@ class DefaultBugReporter @Inject constructor(
// trigger the request
try {
- mBugReportCall = okHttpClient.newCall(request)
+ mBugReportCall = okHttpClient.get().newCall(request)
response = mBugReportCall!!.execute()
responseCode = response.code
} catch (e: Exception) {
diff --git a/features/rageshake/impl/src/main/res/values-sk/translations.xml b/features/rageshake/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..20a6d81aad
--- /dev/null
+++ b/features/rageshake/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,8 @@
+
+
+ "Priložiť snímku obrazovky"
+ "Upraviť snímku obrazovky"
+ "Popíšte chybu…"
+ "Ak je to možné, napíšte popis v angličtine."
+ "Odoslať snímku obrazovky"
+
diff --git a/features/roomdetails/impl/src/main/res/values-sk/translations.xml b/features/roomdetails/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..f7fc1c68a5
--- /dev/null
+++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,28 @@
+
+
+
+ - "1 osoba"
+ - "%1$d ľudia"
+ - "%1$d ľudí"
+
+ "Pridať tému"
+ "Upraviť miestnosť"
+ "Nepodarilo sa nám aktualizovať všetky informácie o tejto miestnosti."
+ "Nepodarilo sa aktualizovať miestnosť"
+ "Šifrovanie správ je zapnuté"
+ "Pozvať ľudí"
+ "Oznámenie"
+ "Názov miestnosti"
+ "Zdieľať miestnosť"
+ "Aktualizácia miestnosti…"
+ "Čaká sa"
+ "Členovia miestnosti"
+ "Zablokovať"
+ "Zablokovať používateľa"
+ "Odblokovať"
+ "Odblokovať používateľa"
+ "Opustiť miestnosť"
+ "Ľudia"
+ "Bezpečnosť"
+ "Téma"
+
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
index 2f3c17fb3a..b366a7eec2 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
@@ -38,7 +38,7 @@ import io.element.android.libraries.core.extensions.orEmpty
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
-import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
+import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
@@ -135,7 +135,7 @@ class RoomListPresenter @Inject constructor(
filteredRoomSummaries.value = updateFilteredRoomSummaries(mappedRoomSummaries.value, filter)
}
- val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
+ val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
return RoomListState(
matrixUser = matrixUser.value,
diff --git a/features/roomlist/impl/src/main/res/values-sk/translations.xml b/features/roomlist/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..22e8fa193f
--- /dev/null
+++ b/features/roomlist/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,7 @@
+
+
+ "Vytvorte novú konverzáciu alebo miestnosť"
+ "Všetky konverzácie"
+ "Vyzerá to tak, že používate nové zariadenie. Overte, či ste to vy, aby ste mali prístup k zašifrovaným správam."
+ "Získajte prístup k histórii vašich správ"
+
diff --git a/features/verifysession/impl/src/main/res/values-sk/translations.xml b/features/verifysession/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..275924e9ec
--- /dev/null
+++ b/features/verifysession/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,19 @@
+
+
+ "Zdá sa, že niečo nie je v poriadku. Časový limit žiadosti vypršal alebo bola žiadosť zamietnutá."
+ "Skontrolujte, či sa emotikony uvedené nižšie zhodujú s emotikonmi zobrazenými vo vašej druhej relácii."
+ "Porovnajte emotikony"
+ "Vaša nová relácia je teraz overená. Má prístup k vašim zašifrovaným správam a ostatní používatelia ju budú vidieť ako dôveryhodnú."
+ "Dokážte, že ste to vy, aby ste získali prístup k histórii vašich zašifrovaných správ."
+ "Otvoriť existujúcu reláciu"
+ "Zopakovať overenie"
+ "Som pripravený/á"
+ "Čaká sa na zhodu"
+ "Porovnajte jedinečné emotikony a uistite sa, že sú zobrazené v rovnakom poradí."
+ "Nezhodujú sa"
+ "Zhodujú sa"
+ "Ak chcete pokračovať, prijmite žiadosť o spustenie procesu overenia vo vašej druhej relácii."
+ "Čaká sa na prijatie žiadosti"
+ "Overovanie zrušené"
+ "Spustiť"
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 97a526d065..dea291fc58 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -6,7 +6,7 @@
android_gradle_plugin = "8.0.2"
kotlin = "1.8.21"
ksp = "1.8.21-1.0.11"
-molecule = "0.9.0"
+molecule = "0.10.0"
# AndroidX
material = "1.9.0"
@@ -39,8 +39,8 @@ datetime = "0.4.0"
serialization_json = "1.5.1"
showkase = "1.0.0-beta18"
jsoup = "1.16.1"
-appyx = "1.2.0"
-dependencycheck = "8.2.1"
+appyx = "1.3.0"
+dependencycheck = "8.3.1"
dependencyanalysis = "1.20.0"
stem = "2.3.0"
sqldelight = "1.5.5"
@@ -142,7 +142,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1"
-matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.23"
+matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.25"
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
@@ -155,6 +155,8 @@ vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0"
vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.1.0"
+maplibre = "org.maplibre.gl:android-sdk:10.2.0"
+maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:1.0.0"
# Analytics
posthog = "com.posthog.android:posthog:2.0.3"
@@ -186,6 +188,7 @@ android_application = { id = "com.android.application", version.ref = "android_g
android_library = { id = "com.android.library", version.ref = "android_gradle_plugin" }
kotlin_android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin_jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+kotlin_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
anvil = { id = "com.squareup.anvil", version.ref = "anvil" }
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt
index 269407d3b5..ea214ff683 100644
--- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt
@@ -17,9 +17,11 @@
package io.element.android.libraries.androidutils.file
import android.content.Context
+import androidx.annotation.WorkerThread
import io.element.android.libraries.core.data.tryOrNull
import timber.log.Timber
import java.io.File
+import java.util.Locale
import java.util.UUID
fun File.safeDelete() {
@@ -52,3 +54,99 @@ fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null):
val suffix = extension?.let { ".$extension" }
return File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() }
}
+
+// Implementation should return true in case of success
+typealias ActionOnFile = (file: File) -> Boolean
+
+/* ==========================================================================================
+ * Log
+ * ========================================================================================== */
+
+fun lsFiles(context: Context) {
+ Timber.v("Content of cache dir:")
+ recursiveActionOnFile(context.cacheDir, ::logAction)
+
+ Timber.v("Content of files dir:")
+ recursiveActionOnFile(context.filesDir, ::logAction)
+}
+
+private fun logAction(file: File): Boolean {
+ if (file.isDirectory) {
+ Timber.v(file.toString())
+ } else {
+ Timber.v("$file ${file.length()} bytes")
+ }
+ return true
+}
+
+/* ==========================================================================================
+ * Private
+ * ========================================================================================== */
+
+/**
+ * Return true in case of success.
+ */
+private fun recursiveActionOnFile(file: File, action: ActionOnFile): Boolean {
+ if (file.isDirectory) {
+ file.list()?.forEach {
+ val result = recursiveActionOnFile(File(file, it), action)
+
+ if (!result) {
+ // Break the loop
+ return false
+ }
+ }
+ }
+
+ return action.invoke(file)
+}
+
+/**
+ * Get the file extension of a fileUri or a filename.
+ *
+ * @param fileUri the fileUri (can be a simple filename)
+ * @return the file extension, in lower case, or null is extension is not available or empty
+ */
+fun getFileExtension(fileUri: String): String? {
+ var reducedStr = fileUri
+
+ if (reducedStr.isNotEmpty()) {
+ // Remove fragment
+ reducedStr = reducedStr.substringBeforeLast('#')
+
+ // Remove query
+ reducedStr = reducedStr.substringBeforeLast('?')
+
+ // Remove path
+ val filename = reducedStr.substringAfterLast('/')
+
+ // Contrary to method MimeTypeMap.getFileExtensionFromUrl, we do not check the pattern
+ // See https://stackoverflow.com/questions/14320527/android-should-i-use-mimetypemap-getfileextensionfromurl-bugs
+ if (filename.isNotEmpty()) {
+ val dotPos = filename.lastIndexOf('.')
+ if (0 <= dotPos) {
+ val ext = filename.substring(dotPos + 1)
+
+ if (ext.isNotBlank()) {
+ return ext.lowercase(Locale.ROOT)
+ }
+ }
+ }
+ }
+
+ return null
+}
+
+/* ==========================================================================================
+ * Size
+ * ========================================================================================== */
+
+@WorkerThread
+fun File.getSizeOfFiles(): Long {
+ return walkTopDown()
+ .onEnter {
+ Timber.v("Get size of ${it.absolutePath}")
+ true
+ }
+ .sumOf { it.length() }
+}
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt
new file mode 100644
index 0000000000..9cd70febcc
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.androidutils.filesize
+
+import android.content.Context
+import android.os.Build
+import android.text.format.Formatter
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.ApplicationContext
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class AndroidFileSizeFormatter @Inject constructor(
+ @ApplicationContext private val context: Context,
+ ) : FileSizeFormatter {
+ override fun format(fileSize: Long, useShortFormat: Boolean): String {
+ // Since Android O, the system considers that 1kB = 1000 bytes instead of 1024 bytes.
+ // We want to avoid that.
+ val normalizedSize = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
+ fileSize
+ } else {
+ // First convert the size
+ when {
+ fileSize < 1024 -> fileSize
+ fileSize < 1024 * 1024 -> fileSize * 1000 / 1024
+ fileSize < 1024 * 1024 * 1024 -> fileSize * 1000 / 1024 * 1000 / 1024
+ else -> fileSize * 1000 / 1024 * 1000 / 1024 * 1000 / 1024
+ }
+ }
+
+ return if (useShortFormat) {
+ Formatter.formatShortFileSize(context, normalizedSize)
+ } else {
+ Formatter.formatFileSize(context, normalizedSize)
+ }
+ }
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/FakeFileSizeFormatter.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/FakeFileSizeFormatter.kt
similarity index 78%
rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/FakeFileSizeFormatter.kt
rename to libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/FakeFileSizeFormatter.kt
index 4ff65b0146..32c0239428 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/FakeFileSizeFormatter.kt
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/FakeFileSizeFormatter.kt
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-package io.element.android.features.messages.timeline
-
-import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter
+package io.element.android.libraries.androidutils.filesize
class FakeFileSizeFormatter : FileSizeFormatter {
- override fun format(fileSize: Long): String {
+ override fun format(fileSize: Long, useShortFormat: Boolean): String {
return "$fileSize Bytes"
}
}
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/FileSizeFormatter.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/FileSizeFormatter.kt
new file mode 100644
index 0000000000..7be38bf9bd
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/FileSizeFormatter.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.androidutils.filesize
+
+interface FileSizeFormatter {
+ /**
+ * Formats a content size to be in the form of bytes, kilobytes, megabytes, etc.
+ */
+ fun format(fileSize: Long, useShortFormat: Boolean = true): String
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt
index 2172f4518a..2195be296f 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt
@@ -17,6 +17,7 @@
package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -26,7 +27,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.progressSemantics
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
@@ -55,7 +55,7 @@ fun PreferenceText(
tintColor: Color? = null,
onClick: () -> Unit = {},
) {
- val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight
+ val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight
Box(
modifier = modifier
.fillMaxWidth()
@@ -69,9 +69,10 @@ fun PreferenceText(
.padding(vertical = preferencePaddingVertical)
) {
PreferenceIcon(icon = icon, tintColor = tintColor)
- Column(modifier = Modifier
- .weight(1f)
- .align(Alignment.CenterVertically)
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .align(Alignment.CenterVertically)
) {
if (title != null) {
Text(
@@ -92,15 +93,24 @@ fun PreferenceText(
}
}
if (currentValue != null) {
- Text(currentValue, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary)
- Spacer(Modifier.width(16.dp))
+ Text(
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .padding(horizontal = 16.dp),
+ text = currentValue,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.secondary,
+ )
} else if (loadingCurrentValue) {
- CircularProgressIndicator(modifier = Modifier
- .progressSemantics()
- .size(20.dp), strokeWidth = 2.dp)
- Spacer(Modifier.width(16.dp))
+ CircularProgressIndicator(
+ modifier = Modifier
+ .progressSemantics()
+ .padding(horizontal = 16.dp)
+ .size(20.dp)
+ .align(Alignment.CenterVertically),
+ strokeWidth = 2.dp
+ )
}
-
}
}
}
@@ -111,9 +121,39 @@ internal fun PreferenceTextPreview() = ElementThemedPreview { ContentToPreview()
@Composable
private fun ContentToPreview() {
- PreferenceText(
- title = "Title",
- subtitle = "Some content",
- icon = Icons.Default.BugReport,
- )
+ Column(
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ PreferenceText(
+ title = "Title",
+ icon = Icons.Default.BugReport,
+ )
+ PreferenceText(
+ title = "Title",
+ subtitle = "Some content",
+ icon = Icons.Default.BugReport,
+ )
+ PreferenceText(
+ title = "Title",
+ subtitle = "Some content",
+ icon = Icons.Default.BugReport,
+ currentValue = "123",
+ )
+ PreferenceText(
+ title = "Title",
+ subtitle = "Some content",
+ icon = Icons.Default.BugReport,
+ loadingCurrentValue = true,
+ )
+ PreferenceText(
+ title = "Title",
+ icon = Icons.Default.BugReport,
+ currentValue = "123",
+ )
+ PreferenceText(
+ title = "Title",
+ icon = Icons.Default.BugReport,
+ loadingCurrentValue = true,
+ )
+ }
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt
index 1de46c78e0..ba9f2d3121 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt
@@ -21,6 +21,7 @@ import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
@@ -28,27 +29,31 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.res.stringResource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
+/**
+ * A global dispatcher of [SnackbarMessage] to be displayed in [Snackbar] via a [SnackbarHostState].
+ */
class SnackbarDispatcher {
private val mutex = Mutex()
- private val snackbarState = MutableStateFlow(null)
- val snackbarMessage: Flow = snackbarState
+ private val _snackbarMessage = MutableStateFlow(null)
+ val snackbarMessage: Flow = _snackbarMessage.asStateFlow()
suspend fun post(message: SnackbarMessage) {
mutex.withLock {
- snackbarState.update { message }
+ _snackbarMessage.update { message }
}
}
suspend fun clear() {
mutex.withLock {
- snackbarState.update { null }
+ _snackbarMessage.update { null }
}
}
}
@@ -59,10 +64,8 @@ val LocalSnackbarDispatcher = compositionLocalOf {
}
@Composable
-fun handleSnackbarMessage(
- snackbarDispatcher: SnackbarDispatcher
-): SnackbarMessage? {
- return snackbarDispatcher.snackbarMessage.collectAsState(initial = null).value
+fun SnackbarDispatcher.collectSnackbarMessageAsState(): State {
+ return snackbarMessage.collectAsState(initial = null)
}
@Composable
diff --git a/libraries/eventformatter/impl/src/main/res/values-sk/translations.xml b/libraries/eventformatter/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..d2aae2bf98
--- /dev/null
+++ b/libraries/eventformatter/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,50 @@
+
+
+ "(obrázok bol tiež zmenený)"
+ "%1$s zmenili svoj obrázok"
+ "Zmenili ste svoj obrázok"
+ "%1$s zmenili svoje zobrazované meno z %2$s na %3$s"
+ "Zmenili ste si zobrazované meno z %1$s na %2$s"
+ "%1$s odstránili svoje zobrazované meno (predtým bolo %2$s)"
+ "Odstránili ste svoje zobrazované meno (predtým bolo %1$s)"
+ "%1$s nastavili svoje zobrazované meno na %2$s"
+ "Svoje zobrazované meno ste nastavili na %1$s"
+ "%1$s zmenil/a obrázok miestnosti"
+ "Zmenili ste obrázok miestnosti"
+ "%1$s odstránil/a obrázok miestnosti"
+ "Odstránili ste obrázok miestnosti"
+ "%1$s zakázal/a používateľa %2$s"
+ "Zakázali ste používateľa %1$s"
+ "%1$s vytvoril/a miestnosť"
+ "Vytvorili ste miestnosť"
+ "%1$s pozval/a používateľa %2$s"
+ "%1$s prijal/a pozvanie"
+ "Prijali ste pozvánku"
+ "Pozvali ste používateľa %1$s"
+ "%1$s vás pozval/a"
+ "%1$s sa pripojil/a do miestnosti"
+ "Vstúpili ste do miestnosti"
+ "%1$s požiadal o pripojenie"
+ "%1$s umožnil/a používateľovi %2$s pripojiť sa"
+ "%1$s vám umožnil/a pripojiť sa"
+ "Požiadali ste o pripojenie"
+ "Zrušili ste svoju žiadosť o pripojenie"
+ "%1$s opustil/a miestnosť"
+ "Opustili ste miestnosť"
+ "%1$s zmenil/a názov miestnosti na: %2$s"
+ "Zmenili ste názov miestnosti na: %1$s"
+ "%1$s odstránil/a názov miestnosti"
+ "Odstránili ste názov miestnosti"
+ "%1$s odmietol/a pozvánku"
+ "Odmietli ste pozvánku"
+ "%1$s odstránil/a %2$s"
+ "Odstránili ste %1$s"
+ "%1$s poslal/a pozvánku používateľovi %2$s, aby sa pripojil k miestnosti"
+ "Poslali ste pozvánku používateľovi %1$s, aby sa pripojil do miestnosti"
+ "%1$s zmenil/a tému na: %2$s"
+ "Zmenili ste tému na: %1$s"
+ "%1$s odstránil/a tému miestnosti"
+ "Odstránili ste tému miestnosti"
+ "%1$s zrušil/a zákaz pre %2$s"
+ "Zrušili ste zákaz pre %1$s"
+
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 93f21c226e..84855adb96 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
@@ -49,6 +49,12 @@ interface MatrixClient : Closeable {
fun sessionVerificationService(): SessionVerificationService
fun pushersService(): PushersService
fun notificationService(): NotificationService
+ suspend fun getCacheSize(): Long
+
+ /**
+ * Will close the client and delete the cache data.
+ */
+ suspend fun clearCache()
suspend fun logout()
suspend fun loadUserDisplayName(): Result
suspend fun loadUserAvatarURLString(): Result
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt
index f52e24d79b..2046252930 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt
@@ -21,5 +21,5 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
interface NotificationService {
- fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result
+ fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId, filterByPushRules: Boolean): Result
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
index f47500f558..0ee2e2863a 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
@@ -116,4 +116,13 @@ interface MatrixRoom : Closeable {
suspend fun setTopic(topic: String): Result
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result
+
+ /**
+ * Share a location message in the room.
+ *
+ * @param body A human readable textual representation of the location.
+ * @param geoUri A geo URI (RFC 5870) representing the location e.g. `geo:51.5008,0.1247;u=35`.
+ * Respectively: latitude, longitude, and (optional) uncertainty.
+ */
+ suspend fun sendLocation(body: String, geoUri: String): Result
}
diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts
index 1ac5b4b507..5709b5a6d7 100644
--- a/libraries/matrix/impl/build.gradle.kts
+++ b/libraries/matrix/impl/build.gradle.kts
@@ -32,6 +32,7 @@ dependencies {
// api(projects.libraries.rustsdk)
implementation(libs.matrix.sdk)
implementation(projects.libraries.di)
+ implementation(projects.libraries.androidutils)
implementation(projects.services.toolbox.api)
api(projects.libraries.matrix.api)
implementation(libs.dagger)
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 baba3fd638..127c1c0adb 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
@@ -16,6 +16,8 @@
package io.element.android.libraries.matrix.impl
+import io.element.android.libraries.androidutils.file.getSizeOfFiles
+import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScopeOf
import io.element.android.libraries.matrix.api.MatrixClient
@@ -229,15 +231,25 @@ class RustMatrixClient constructor(
client.destroy()
}
+ override suspend fun getCacheSize(): Long {
+ // Do not use client.userId since it can throw if client has been closed (during clear cache)
+ return baseDirectory.getCacheSize(userID = sessionId.value)
+ }
+
+ override suspend fun clearCache() {
+ close()
+ baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false)
+ }
+
override suspend fun logout() = withContext(dispatchers.io) {
try {
client.logout()
} catch (failure: Throwable) {
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
}
- baseDirectory.deleteSessionDirectory(userID = client.userId())
- sessionStore.removeSession(client.userId())
close()
+ baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
+ sessionStore.removeSession(sessionId.value)
}
override suspend fun loadUserDisplayName(): Result = withContext(dispatchers.io) {
@@ -271,11 +283,48 @@ class RustMatrixClient constructor(
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
- private fun File.deleteSessionDirectory(userID: String): Boolean {
+ private suspend fun File.getCacheSize(
+ userID: String,
+ includeCryptoDb: Boolean = false,
+ ): Long = withContext(dispatchers.io) {
// Rust sanitises the user ID replacing invalid characters with an _
val sanitisedUserID = userID.replace(":", "_")
- val sessionDirectory = File(this, sanitisedUserID)
- return sessionDirectory.deleteRecursively()
+ val sessionDirectory = File(this@getCacheSize, sanitisedUserID)
+ if (includeCryptoDb) {
+ sessionDirectory.getSizeOfFiles()
+ } else {
+ listOf(
+ "matrix-sdk-state.sqlite3",
+ "matrix-sdk-state.sqlite3-shm",
+ "matrix-sdk-state.sqlite3-wal",
+ ).map { fileName ->
+ File(sessionDirectory, fileName)
+ }.sumOf { file ->
+ file.length()
+ }
+ }
+ }
+
+ private suspend fun File.deleteSessionDirectory(
+ userID: String,
+ deleteCryptoDb: Boolean = false,
+ ): Boolean = withContext(dispatchers.io) {
+ // Rust sanitises the user ID replacing invalid characters with an _
+ val sanitisedUserID = userID.replace(":", "_")
+ val sessionDirectory = File(this@deleteSessionDirectory, sanitisedUserID)
+ if (deleteCryptoDb) {
+ // Delete the folder and all its content
+ sessionDirectory.deleteRecursively()
+ } else {
+ // Delete only the state.db file
+ sessionDirectory.listFiles().orEmpty()
+ .filter { it.name.contains("matrix-sdk-state") }
+ .forEach { file ->
+ Timber.w("Deleting file ${file.name}...")
+ file.safeDelete()
+ }
+ true
+ }
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt
index 52d1598cff..99e5991719 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt
@@ -32,10 +32,11 @@ class RustNotificationService(
override fun getNotification(
userId: SessionId,
roomId: RoomId,
- eventId: EventId
+ eventId: EventId,
+ filterByPushRules: Boolean,
): Result {
return runCatching {
- client.getNotificationItem(roomId.value, eventId.value)?.use(notificationMapper::map)
+ client.getNotificationItem(roomId.value, eventId.value, filterByPushRules)?.use(notificationMapper::map)
}
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
index 39d829c6a8..4711fe6864 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
@@ -374,4 +374,13 @@ class RustMatrixRoom(
}
}
}
+
+ override suspend fun sendLocation(
+ body: String,
+ geoUri: String
+ ): Result = withContext(coroutineDispatchers.io) {
+ runCatching {
+ innerRoom.sendLocation(body, geoUri, genTransactionId())
+ }
+ }
}
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 58bc6c1ff1..cfc6e14fd8 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
@@ -101,6 +101,13 @@ class FakeMatrixClient(
override fun syncService() = syncService
+ override suspend fun getCacheSize(): Long {
+ return 0
+ }
+
+ override suspend fun clearCache() {
+ }
+
override suspend fun logout() {
delay(100)
logoutFailure?.let { throw it }
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt
index 816bfc572a..81fa3b677c 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt
@@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.flowOf
val A_OIDC_DATA = OidcDetails(url = "a-url")
class FakeAuthenticationService : MatrixAuthenticationService {
- private var homeserver = MutableStateFlow(null)
+ private val homeserver = MutableStateFlow(null)
private var oidcError: Throwable? = null
private var oidcCancelError: Throwable? = null
private var loginError: Throwable? = null
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt
index b92c9b7a92..9eb5a20ba4 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt
@@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService
class FakeNotificationService : NotificationService {
- override fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result {
+ override fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId, filterByPushRules: Boolean): Result {
return Result.success(null)
}
}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
index d8187b0a1d..6fab4cb053 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
@@ -77,6 +77,7 @@ class FakeMatrixRoom(
private var cancelSendResult = Result.success(Unit)
private var forwardEventResult = Result.success(Unit)
private var reportContentResult = Result.success(Unit)
+ private var sendLocationResult = Result.success(Unit)
var sendMediaCount = 0
private set
@@ -93,6 +94,9 @@ class FakeMatrixRoom(
var reportedContentCount: Int = 0
private set
+ var sendLocationCount: Int = 0
+ private set
+
var isInviteAccepted: Boolean = false
private set
@@ -262,6 +266,14 @@ class FakeMatrixRoom(
return reportContentResult
}
+ override suspend fun sendLocation(
+ body: String,
+ geoUri: String
+ ): Result = simulateLongTask {
+ sendLocationCount++
+ return sendLocationResult
+ }
+
override fun close() = Unit
fun givenLeaveRoomError(throwable: Throwable?) {
@@ -355,4 +367,8 @@ class FakeMatrixRoom(
fun givenReportContentResult(result: Result) {
reportContentResult = result
}
+
+ fun givenSendLocationResult(result: Result) {
+ sendLocationResult = result
+ }
}
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt
index 9038e03611..c5b7f1ed44 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt
@@ -26,16 +26,17 @@ import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient
import okhttp3.OkHttpClient
import javax.inject.Inject
+import javax.inject.Provider
class LoggedInImageLoaderFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val matrixClient: MatrixClient,
- private val okHttpClient: OkHttpClient,
+ private val okHttpClient: Provider,
) : ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
return ImageLoader
.Builder(context)
- .okHttpClient(okHttpClient)
+ .okHttpClient { okHttpClient.get() }
.components {
// Add gif support
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -54,12 +55,12 @@ class LoggedInImageLoaderFactory @Inject constructor(
class NotLoggedInImageLoaderFactory @Inject constructor(
@ApplicationContext private val context: Context,
- private val okHttpClient: OkHttpClient,
+ private val okHttpClient: Provider,
) : ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
return ImageLoader
.Builder(context)
- .okHttpClient(okHttpClient)
+ .okHttpClient { okHttpClient.get() }
.build()
}
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
index fb3fcfc61f..faa8eb86d8 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
@@ -59,6 +59,7 @@ class NotifiableEventResolver @Inject constructor(
userId = sessionId,
roomId = roomId,
eventId = eventId,
+ filterByPushRules = true,
)
}.fold(
{
diff --git a/libraries/push/impl/src/main/res/values-sk/translations.xml b/libraries/push/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..71a225140e
--- /dev/null
+++ b/libraries/push/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,50 @@
+
+
+ "Zavolať"
+ "Tiché oznámenia"
+ "** Nepodarilo sa odoslať - prosím otvorte miestnosť"
+ "Pripojiť sa"
+ "Zamietnuť"
+ "Nové správy"
+ "Označiť ako prečítané"
+ "Prezeráte si oznámenie! Kliknite na mňa!"
+ "%1$s: %2$s"
+ "%1$s: %2$s %3$s"
+ "%1$s a %2$s"
+ "%1$s v %2$s"
+ "%1$s v %2$s a %3$s"
+
+ - "%1$s: %2$d správa"
+ - "%1$s: %2$d správy"
+ - "%1$s: %2$d správ"
+
+
+ - "%d oznámenie"
+ - "%d oznámenia"
+ - "%d oznámení"
+
+
+ - "%d pozvánka"
+ - "%d pozvánky"
+ - "%d pozvánok"
+
+
+ - "%d nová správa"
+ - "%d nové správy"
+ - "%d nových správ"
+
+
+ - "%d neprečítaná oznámená správa"
+ - "%d neprečítané oznámené správy"
+ - "%d neprečítaných oznámených správ"
+
+
+ - "%d miestnosť"
+ - "%d miestnosti"
+ - "%d miestností"
+
+ "Vyberte spôsob prijímania oznámení"
+ "Synchronizácia na pozadí"
+ "Služby Google"
+ "Rýchla odpoveď"
+
diff --git a/libraries/textcomposer/src/main/res/values-sk/translations.xml b/libraries/textcomposer/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..e63bd21442
--- /dev/null
+++ b/libraries/textcomposer/src/main/res/values-sk/translations.xml
@@ -0,0 +1,9 @@
+
+
+ "Správa…"
+ "Použiť tučný formát"
+ "Použiť formát kurzívy"
+ "Použiť formát prečiarknutia"
+ "Použiť formát podčiarknutia"
+ "Nastaviť odkaz"
+
diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml
index 70a3c7a3ab..3fdcf39458 100644
--- a/libraries/ui-strings/src/main/res/values-cs/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml
@@ -28,6 +28,7 @@
"Pozvat"
"Pozvat přátele"
"Pozvat přátele do %1$s"
+ "Pozvat lidi na %1$s"
"Pozvánky"
"Zjistit více"
"Odejít"
@@ -108,6 +109,7 @@
"Server není podporován"
"URL serveru"
"Nastavení"
+ "Sdílená poloha"
"Zahajování chatu…"
"Nálepka"
"Úspěch"
@@ -163,6 +165,8 @@
"Nahrání média se nezdařilo, zkuste to prosím znovu."
"Nahrání média se nezdařilo, zkuste to prosím znovu."
"Zaškrtněte, pokud chcete skrýt všechny aktuální a budoucí zprávy od tohoto uživatele"
+ "Sdílet polohu"
+ "Sdílet moji polohu"
"Rageshake"
"Práh detekce"
"Obecné"
diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml
index f8d004d624..3129b91f23 100644
--- a/libraries/ui-strings/src/main/res/values-de/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-de/translations.xml
@@ -158,7 +158,7 @@
"Teile Analyse-Daten"
"Medienauswahl fehlgeschlagen, bitte versuche es erneut."
"Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuchen Sie es erneut."
- "Medien hochladen fehlgeschlagen. Bitte versuchen Sie es erneut."
+ "Hochladen von Medien fehlgeschlagen, bitte versuchen Sie es erneut."
"Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest"
"Rageshake"
"Erkennungsschwelle"
diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml
new file mode 100644
index 0000000000..d7cb30a8b2
--- /dev/null
+++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml
@@ -0,0 +1,158 @@
+
+
+ "Skryť heslo"
+ "Odoslať súbory"
+ "Zobraziť heslo"
+ "Používateľské menu"
+ "Prijať"
+ "Späť"
+ "Zrušiť"
+ "Vybrať fotku"
+ "Vyčistiť"
+ "Zavrieť"
+ "Dokončiť overenie"
+ "Potvrdiť"
+ "Pokračovať"
+ "Kopírovať"
+ "Kopírovať odkaz"
+ "Kopírovať odkaz do správy"
+ "Vytvoriť"
+ "Vytvoriť miestnosť"
+ "Odmietnuť"
+ "Vypnúť"
+ "Hotovo"
+ "Upraviť"
+ "Povoliť"
+ "Zabudnuté heslo?"
+ "Pozvať"
+ "Pozvať priateľov"
+ "Pozvať priateľov do %1$s"
+ "Pozvať ľudí do %1$s"
+ "Pozvánky"
+ "Zistiť viac"
+ "Opustiť"
+ "Opustiť miestnosť"
+ "Ďalej"
+ "Nie"
+ "Teraz nie"
+ "OK"
+ "Otvoriť pomocou"
+ "Rýchla odpoveď"
+ "Citovať"
+ "Odstrániť"
+ "Odpovedať"
+ "Nahlásiť chybu"
+ "Nahlásiť obsah"
+ "Skúsiť znova"
+ "Opakovať dešifrovanie"
+ "Uložiť"
+ "Hľadať"
+ "Odoslať"
+ "Odoslať správu"
+ "Zdieľať"
+ "Zdieľať odkaz"
+ "Preskočiť"
+ "Spustiť"
+ "Začať konverzáciu"
+ "Spustiť overovanie"
+ "Ťuknutím načítate mapu"
+ "Urobiť fotku"
+ "Zobraziť zdroj"
+ "Áno"
+ "O aplikácii"
+ "Analytika"
+ "Zvuk"
+ "Bubliny"
+ "Autorské práva"
+ "Vytváranie miestnosti…"
+ "Opustil/a miestnosť"
+ "Chyba dešifrovania"
+ "Možnosti pre vývojárov"
+ "(upravené)"
+ "Upravuje sa"
+ "* %1$s %2$s"
+ "Šifrovanie zapnuté"
+ "Chyba"
+ "Súbor"
+ "Súbor bol uložený do priečinka Stiahnuté súbory"
+ "Preposlať správu"
+ "GIF"
+ "Obrázok"
+ "Nedokážeme overiť Matrix ID tohto používateľa. Pozvánka nemusí byť prijatá."
+ "Odkaz bol skopírovaný do schránky"
+ "Načítava sa…"
+ "Správa"
+ "Rozloženie správy"
+ "Správa odstránená"
+ "Moderné"
+ "Žiadne výsledky"
+ "Offline"
+ "Heslo"
+ "Ľudia"
+ "Trvalý odkaz"
+ "Reakcie"
+ "Nahlásiť chybu"
+ "Nahlásenie bolo odoslané"
+ "Názov miestnosti"
+ "Výsledky hľadania"
+ "Bezpečnosť"
+ "Vyberte svoj server"
+ "Odosiela sa…"
+ "Server nie je podporovaný"
+ "URL adresa servera"
+ "Nastavenia"
+ "Zdieľaná poloha"
+ "Nálepka"
+ "Úspech"
+ "Návrhy"
+ "Synchronizuje sa"
+ "Téma"
+ "O čom je táto miestnosť?"
+ "Nie je možné dešifrovať"
+ "Nepodporovaná udalosť"
+ "Používateľské meno"
+ "Overovanie zrušené"
+ "Overovanie je dokončené"
+ "Video"
+ "Čaká sa…"
+ "Potvrdenie"
+ "Upozornenie"
+ "Aktivity"
+ "Vlajky"
+ "Jedlo a nápoje"
+ "Zvieratá a príroda"
+ "Predmety"
+ "Smajlíky a ľudia"
+ "Cestovanie a miesta"
+ "Symboly"
+ "Načítanie správ zlyhalo"
+ "Niektoré správy neboli odoslané"
+ "Prepáčte, vyskytla sa chyba"
+ "🔐️ Pripojte sa ku mne na %1$s"
+ "Ahoj, porozprávajte sa so mnou na %1$s: %2$s"
+ "Ste si istí, že chcete opustiť túto miestnosť? Ste tu jediná osoba. Ak odídete, nikto sa do nej nebude môcť v budúcnosti pripojiť, vrátane vás."
+ "Ste si istí, že chcete opustiť túto miestnosť? Táto miestnosť nie je verejná a bez pozvania sa do nej nebudete môcť vrátiť."
+ "Ste si istí, že chcete opustiť miestnosť?"
+ "%1$s Android"
+
+ - "%1$d člen"
+ - "%1$d členovia"
+ - "%1$d členov"
+
+ "Zúrivo potriasť pre nahlásenie chyby"
+ "Zdá sa, že zúrivo trasiete telefónom. Chcete otvoriť obrazovku s nahlásením chýb?"
+ "Toto je začiatok %1$s."
+ "Toto je začiatok tejto konverzácie."
+ "Nové"
+ "Zdieľať polohu"
+ "Zdieľať moju polohu"
+ "Zdieľajte túto polohu"
+ "Zúrivé potrasenie"
+ "Prahová hodnota detekcie"
+ "Všeobecné"
+ "Verzia: %1$s (%2$s)"
+ "sk"
+ "Chyba"
+ "Úspech"
+ "Zablokovať používateľa"
+
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index bcd90cb160..b452f153e7 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -164,6 +164,9 @@
"Failed processing media to upload, please try again."
"Failed uploading media, please try again."
"Check if you want to hide all current and future messages from this user"
+ "Share location"
+ "Share my location"
+ "Share this location"
"Rageshake"
"Detection threshold"
"General"
diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
index e4cb389d94..5d8e0bfd1c 100644
--- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
+++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
@@ -43,6 +43,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
+import timber.log.Timber
import java.util.Locale
class RoomListScreen(
@@ -106,8 +107,10 @@ class RoomListScreen(
)
DisposableEffect(Unit) {
+ Timber.w("Start sync!")
matrixClient.startSync()
onDispose {
+ Timber.w("Stop sync!")
matrixClient.stopSync()
}
}
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..04f26d7d81
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:407a17fca1405575861b7c8861222a5d529778eeaeb904c3058fe19ff9f809fc
+size 143328
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..6aebd55728
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:95068257a39fc8a693adce87c54b605be395de5fd639ee00b7d34dde5bce568a
+size 147055
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..97787cadcd
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:819c585286d2ef5c7064766d537b9da62be54b67340a5a7a44e94dcf53b1caf4
+size 277318
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..7efba0b598
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api.internal_null_DefaultGroup_StaticMapPlaceholderLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c0e68e68e1cf4f735671b5abcfb4e0c936ca1a00ead817e8f010d39f5b753252
+size 280801
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewDarkPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..78ba79757e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewDarkPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5d34ac54f8c46bc3752366adeefb0952b23f052f9359b48864a6a43455334a6d
+size 4965
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewLightPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..665c8811ac
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_MapViewLightPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
+size 4457
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewDarkPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..04f26d7d81
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewDarkPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:407a17fca1405575861b7c8861222a5d529778eeaeb904c3058fe19ff9f809fc
+size 143328
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewLightPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..97787cadcd
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.api_null_DefaultGroup_StaticMapViewLightPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:819c585286d2ef5c7064766d537b9da62be54b67340a5a7a44e94dcf53b1caf4
+size 277318
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index 57b4a846e2..db838e0f1f 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5d806cbab0f26fb4f471adb5fbecfc600603a7651c5391501d42b13b23617a4c
-size 29301
+oid sha256:9597821bbe6b65693470b40e5f570cf318821d2dbf5bdf525d447daff7d352ae
+size 35345
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..db838e0f1f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9597821bbe6b65693470b40e5f570cf318821d2dbf5bdf525d447daff7d352ae
+size 35345
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
index d8d2dbc8d1..9786515e1c 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d04eb5d2ebb8b740edcaac00912994e8c164e7b5083595d4085d350af0ca6c33
-size 28482
+oid sha256:61880d7b08cc92743a12a74246495039b76e1e80e2704839f726329efb6958a0
+size 34308
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..9786515e1c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:61880d7b08cc92743a12a74246495039b76e1e80e2704839f726329efb6958a0
+size 34308
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png
index 719089d8e9..cf208695d7 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:04e83ead8c23ec9160eb9cfd656aa5e9bf8c302dcf435d00b78aa2eba3243f0d
-size 64181
+oid sha256:05dc60cbfa8de0e27acecc5d44f7e1841d15f1cdcb749dcf23995aa49d40d924
+size 64174
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png
index dcdf364d71..9d69f48d15 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b59df47ab2ad44752eb3441ed117fff517e42c2c08f5e0c6402168223f35daef
-size 53042
+oid sha256:dc5b5b7a08b61201bde775880eadd40dc51773fe616209c85c143fc703091cef
+size 53024
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png
index ef99f706a2..cfb41488ee 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:002fa211918a6d41c993339b30261fb8b564df442756bafe9f8866d2ecf51c81
-size 54256
+oid sha256:51f3ee04917b0725bc0a721ac770636b7c273c1ef6f800c5ff3087ea59906396
+size 54267
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png
index bfc84aa2bf..2c2e946b6d 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ec598fdc25acf4300e9e44a7b4913e3b9dd881cd27222393047421cd8c361217
-size 54722
+oid sha256:05e6a676f742d70c75d1e4ed35667aadbf22e0d796ef165b5265e8889c5bd062
+size 54715
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png
index c64bff1e9f..dd3e5c1a5e 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:df7c6f78b6a54d1444388aafdd76c8c00ba15be688e2ee10f0582339e74e2499
-size 67872
+oid sha256:51fb6dadd1af1bc140877f205d57270ff67a46a3b4c4508ddcdf11e2402f60e4
+size 67859
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png
index d4c96e56b4..85103e6e4d 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:872c9b5285190577292485c2c0439ceb2259b68740591f1cad7129f322b089b3
-size 57677
+oid sha256:1471ebd62c5514c04bb943e83ae1f2bf51edad409da5fb9c8f6613fbae09d333
+size 57679
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_8,NEXUS_5,1.0,en].png
index 29f39d40c7..b101fabb04 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_8,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_8,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d3bdab2714bd1b232b255fbe0799209da5f40a390384fb21f56bbc1788032129
-size 64512
+oid sha256:c164ad24729e25e5b4a1031bb6bb452516e9172b65d867d584efa63e5a095355
+size 64505
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png
index a12afbc7c9..625da9d324 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a9a1a5bb3c04fb601b84a2b1373544e8eba94f688d938a990308a0e788d96445
-size 61182
+oid sha256:e651c80d07b153dde903f427b4e41e3475c51b524d877ffe55b265ffaac8c32a
+size 61197
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png
index 79ebe73047..9e9b406f63 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b7ee7551b233428723d79b099d7bac4634188a3ac120ff3389b2ed33625dcc94
-size 51252
+oid sha256:cb6a8ad69606bac57df9e1c758756eaf09e887875989546c8aa628207926b79e
+size 51274
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png
index d0c208aa7c..f70b484141 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:48e7307ffc1d8f7f7a885e67851314c3b135e74a4268ff5fede59758f91a3358
-size 51926
+oid sha256:75eab912dfea0e0eac42742dc131d1630270ac91bf840986be56a755765ee18a
+size 51917
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png
index 2a02579eb9..faf29851e2 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:632f982253a6e6f0101fd60a0ef48601cc2c06268ab3d66db084571a5861a52c
-size 52417
+oid sha256:d0d5bee6b1b4b6069a55c0f297a864979b7c1f1f212bc5bccf8806bd82004abc
+size 52429
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png
index 43fe21a48a..73cdbe63be 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c49a225d80fe15170e6444271b63324f67099bb733eb1c4b989508a4d0d678ef
-size 64502
+oid sha256:4c2ce12fd0e6edff9ab4a468ec9f3eab2a206b7c4c51b26fa9fa7bf5a5efdc63
+size 64472
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png
index 4975863ccb..7c14d499d2 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d8612af1823d149e503e5d47814ecb171a4b2a4be19a243751c89034a1fdb30d
-size 55076
+oid sha256:2e000d7940a2fae0a350d508df8b5c95a27f76f60a865cd7372b8a32d89318f6
+size 55082
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_8,NEXUS_5,1.0,en].png
index 885fc65fb2..e34c095393 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_8,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_8,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:40ecdf8314109687437223860b5e28847d0cb8ba7d546829de5f061640e58315
-size 61500
+oid sha256:86cca8af9ab505cd9cd1ae4a941d5053f2d3155eb033ed1121e7d15f92a6cb43
+size 61509
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_Preferences_PreferenceTextPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_Preferences_PreferenceTextPreview_0_null,NEXUS_5,1.0,en].png
index d818c6f2a0..21db86fe46 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_Preferences_PreferenceTextPreview_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_Preferences_PreferenceTextPreview_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1fc465aa6658ace0327804f04e329cae12bf358daca27e48a8ae6bd516752a9c
-size 12843
+oid sha256:197b1b5fa33ba31f4e47f70b12e4b6eaf7fb3ea30368e96b7dec08f37bdeb62c
+size 28185