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