Merge branch 'develop' into feature/fga/room_list_api
This commit is contained in:
commit
2a24d0196e
113 changed files with 2259 additions and 132 deletions
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_analytics_prompt_data_usage">"Nezaznamenávame ani neprofilujeme žiadne osobné údaje"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Môžete to kedykoľvek vypnúť"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"Vaše údaje nebudeme zdieľať s tretími stranami"</string>
|
||||
<string name="screen_analytics_prompt_title">"Pomôžte zlepšiť %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -29,9 +29,9 @@ class FakeAnalyticsService(
|
|||
didAskUserConsent: Boolean = false
|
||||
): AnalyticsService {
|
||||
|
||||
private var isEnabledFlow = MutableStateFlow(isEnabled)
|
||||
private var didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
|
||||
var capturedEvents = mutableListOf<VectorAnalyticsEvent>()
|
||||
private val isEnabledFlow = MutableStateFlow(isEnabled)
|
||||
private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
|
||||
val capturedEvents = mutableListOf<VectorAnalyticsEvent>()
|
||||
|
||||
override fun getAvailableAnalyticsProviders(): List<AnalyticsProvider> = emptyList()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_create_room_action_create_room">"Neuer Raum"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Freunde zu Element einladen"</string>
|
||||
<string name="screen_create_room_add_people_title">"Personen einladen"</string>
|
||||
<string name="screen_create_room_add_people_title">"Personen hinzufügen"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Beim Erstellen des Raums ist ein Fehler aufgetreten"</string>
|
||||
<string name="screen_create_room_private_option_description">"Die Nachrichten in diesem Raum sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden."</string>
|
||||
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_create_room_action_create_room">"Nová miestnosť"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Pozvať priateľov na Element"</string>
|
||||
<string name="screen_create_room_add_people_title">"Pozvať ľudí"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Pri vytváraní miestnosti došlo k chybe"</string>
|
||||
<string name="screen_create_room_private_option_description">"Správy v tejto miestnosti sú šifrované. Šifrovanie už potom nie je možné vypnúť."</string>
|
||||
<string name="screen_create_room_private_option_title">"Súkromná miestnosť (len pre pozvaných)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Správy nie sú šifrované a môže si ich prečítať ktokoľvek. Šifrovanie môžete zapnúť neskôr."</string>
|
||||
<string name="screen_create_room_public_option_title">"Verejná miestnosť (ktokoľvek)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Názov miestnosti"</string>
|
||||
<string name="screen_create_room_topic_label">"Téma (voliteľné)"</string>
|
||||
<string name="screen_create_room_title">"Vytvoriť miestnosť"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_decline_chat_title">"Odmietnuť pozvanie"</string>
|
||||
<string name="screen_invites_empty_list">"Žiadne pozvánky"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) vás pozval/a"</string>
|
||||
</resources>
|
||||
|
|
@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
|
||||
class FakeSeenInvitesStore : SeenInvitesStore {
|
||||
|
||||
private var existing = MutableStateFlow(emptySet<RoomId>())
|
||||
private val existing = MutableStateFlow(emptySet<RoomId>())
|
||||
private var provided: Set<RoomId>? = null
|
||||
|
||||
fun publishRoomIds(invites: Set<RoomId>) {
|
||||
|
|
|
|||
53
features/location/api/build.gradle.kts
Normal file
53
features/location/api/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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<MapRefs?>(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<MapState.Marker> = emptyList<MapState.Marker>().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<Marker> = emptyList<Marker>().toImmutableList(), // The pin's location, if any.
|
||||
) {
|
||||
var position: CameraPosition by mutableStateOf(position)
|
||||
var location: Location? by mutableStateOf(location)
|
||||
var markers: ImmutableList<Marker> 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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Boolean> {
|
||||
override val values: Sequence<Boolean>
|
||||
get() = sequenceOf(true, false)
|
||||
}
|
||||
BIN
features/location/api/src/main/res/drawable/blurred_map_dark.png
Normal file
BIN
features/location/api/src/main/res/drawable/blurred_map_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
23
features/location/api/src/main/res/drawable/pin.xml
Normal file
23
features/location/api/src/main/res/drawable/pin.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="50dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="50"
|
||||
android:viewportHeight="108">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h50v108h-50z"/>
|
||||
<path
|
||||
android:pathData="M25,54L18.94,48L31.06,48L25,54Z"
|
||||
android:fillColor="#1B1D22"/>
|
||||
<path
|
||||
android:pathData="M25,25m-25,0a25,25 0,1 1,50 0a25,25 0,1 1,-50 0"
|
||||
android:fillColor="#1B1D22"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M13,13h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M25,13C20.36,13 16.6,16.86 16.6,21.63C16.6,26.77 21.9,33.86 24.09,36.56C24.57,37.15 25.44,37.15 25.92,36.56C28.1,33.86 33.4,26.77 33.4,21.63C33.4,16.86 29.64,13 25,13ZM25,24.71C23.34,24.71 22,23.33 22,21.63C22,19.93 23.34,18.55 25,18.55C26.66,18.55 28,19.93 28,21.63C28,23.33 26.66,24.71 25,24.71Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
52
features/location/fake/build.gradle.kts
Normal file
52
features/location/fake/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<Location> = flow {
|
||||
while (true) {
|
||||
delay(1_000)
|
||||
emit(aLocation())
|
||||
}
|
||||
}
|
||||
|
||||
private fun aLocation() = Location(
|
||||
lat = 51.49404,
|
||||
lon = -0.25484,
|
||||
accuracy = 5f
|
||||
)
|
||||
54
features/location/impl/build.gradle.kts
Normal file
54
features/location/impl/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
21
features/location/impl/src/main/AndroidManifest.xml
Normal file
21
features/location/impl/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
</manifest>
|
||||
|
|
@ -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<Location> = 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()
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<string name="screen_change_account_provider_other">"Andere"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Arbeitskonto."</string>
|
||||
<string name="screen_change_account_provider_title">"Kontoanbieter ändern"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"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."</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"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."</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"Dieser Server unterstützt derzeit keine Sliding Sync."</string>
|
||||
<string name="screen_change_server_form_header">"Homeserver-URL"</string>
|
||||
<string name="screen_change_server_form_notice">"Du kannst dich nur mit einem existierenden Server verbinden, der Sliding Sync unterstützt. Dein Homeserver-Administrator muss es konfigurieren. %1$s"</string>
|
||||
|
|
|
|||
23
features/login/impl/src/main/res/values-sk/translations.xml
Normal file
23
features/login/impl/src/main/res/values-sk/translations.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_account_provider_continue">"Pokračovať"</string>
|
||||
<string name="screen_account_provider_form_hint">"Adresa domovského servera"</string>
|
||||
<string name="screen_change_server_form_header">"Adresa URL domovského servera"</string>
|
||||
<string name="screen_change_server_subtitle">"Aká je adresa vášho servera?"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Tento účet bol deaktivovaný."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Nesprávne používateľské meno a/alebo heslo"</string>
|
||||
<string name="screen_login_form_header">"Zadajte svoje údaje"</string>
|
||||
<string name="screen_login_server_header">"Kde žijú vaše rozhovory"</string>
|
||||
<string name="screen_login_title">"Vitajte späť!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Prihlásiť sa do %1$s"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Zmeniť poskytovateľa účtu"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Súkromný server pre zamestnancov spoločnosti Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Chystáte sa prihlásiť do %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Chystáte sa vytvoriť účet na %1$s"</string>
|
||||
<string name="screen_change_server_submit">"Pokračovať"</string>
|
||||
<string name="screen_change_server_title">"Vyberte svoj server"</string>
|
||||
<string name="screen_login_password_hint">"Heslo"</string>
|
||||
<string name="screen_login_submit">"Pokračovať"</string>
|
||||
<string name="screen_login_username_hint">"Používateľské meno"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_signout_confirmation_dialog_content">"Ste si istí, že sa chcete odhlásiť?"</string>
|
||||
<string name="screen_signout_confirmation_dialog_title">"Odhlásiť sa"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"Prebieha odhlasovanie…"</string>
|
||||
<string name="screen_signout_confirmation_dialog_submit">"Odhlásiť sa"</string>
|
||||
<string name="screen_signout_preference_item">"Odhlásiť sa"</string>
|
||||
</resources>
|
||||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Async<LocalMedia>> = remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
}
|
||||
val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
|
||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||
localMediaActions.Configure()
|
||||
DisposableEffect(loadMediaTrigger) {
|
||||
coroutineScope.downloadMedia(mediaFile, localMedia)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@
|
|||
<string name="screen_room_attachment_source_camera_video">"Natočit video"</string>
|
||||
<string name="screen_room_attachment_source_files">"Příloha"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Knihovna fotografií a videí"</string>
|
||||
<string name="screen_room_attachment_source_location">"Poloha"</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Nepodařilo se načíst údaje o uživateli"</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Chtěli byste je pozvat zpět?"</string>
|
||||
<string name="screen_room_invite_again_alert_title">"V tomto chatu jste sami"</string>
|
||||
<string name="screen_room_no_permission_to_post">"Nemáte oprávnění vkládat příspěvky do této místnosti"</string>
|
||||
<string name="screen_room_message_copied">"Zpráva zkopírována"</string>
|
||||
<string name="screen_room_no_permission_to_post">"Nemáte oprávnění zveřejňovat příspěvky v této místnosti"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Odeslat znovu"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Vaši zprávu se nepodařilo odeslat"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d zmena miestnosti"</item>
|
||||
<item quantity="few">"%1$d zmeny miestnosti"</item>
|
||||
<item quantity="other">"%1$d zmien miestnosti"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_attachment_source_camera">"Kamera"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Odfotiť"</string>
|
||||
<string name="screen_room_attachment_source_camera_video">"Nahrať video"</string>
|
||||
<string name="screen_room_attachment_source_files">"Príloha"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Knižnica fotografií a videí"</string>
|
||||
<string name="screen_room_attachment_source_location">"Poloha"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Odoslať znova"</string>
|
||||
<string name="screen_room_retry_send_menu_remove_action">"Odstrániť"</string>
|
||||
</resources>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
<string name="screen_room_attachment_source_camera_video">"Record a video"</string>
|
||||
<string name="screen_room_attachment_source_files">"Attachment"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Photo & Video Library"</string>
|
||||
<string name="screen_room_attachment_source_location">"Location"</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Would you like to invite them back?"</string>
|
||||
<string name="screen_room_invite_again_alert_title">"You are alone in this chat"</string>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<string name="screen_onboarding_sign_in_with_qr_code">"Přihlásit se pomocí QR kódu"</string>
|
||||
<string name="screen_onboarding_sign_up">"Vytvořit účet"</string>
|
||||
<string name="screen_onboarding_subtitle">"Komunikujte a spolupracujte bezpečně"</string>
|
||||
<string name="screen_onboarding_welcome_message">"Vítejte u dosud nejrychlejšího Elementu. Vylepšený pro rychlost a jednoduchost."</string>
|
||||
<string name="screen_onboarding_welcome_subtitle">"Vítejte v %1$s. Vylepšený, pro rychlost a jednoduchost."</string>
|
||||
<string name="screen_onboarding_welcome_title">"Buďte ve svém živlu"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_onboarding_sign_up">"Vytvoriť účet"</string>
|
||||
</resources>
|
||||
|
|
@ -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<Int>
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Int> {
|
||||
return cacheIndexState
|
||||
}
|
||||
|
||||
fun incrementCacheIndex() {
|
||||
cacheIndexState.value++
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DeveloperSettingsState> {
|
||||
|
||||
@Composable
|
||||
|
|
@ -47,6 +55,12 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
val enabledFeatures = remember {
|
||||
mutableStateMapOf<String, Boolean>()
|
||||
}
|
||||
val cacheSize = remember {
|
||||
mutableStateOf<Async<String>>(Async.Uninitialized)
|
||||
}
|
||||
val clearCacheAction = remember {
|
||||
mutableStateOf<Async<Unit>>(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<Async<String>>) = launch {
|
||||
suspend {
|
||||
computeCacheSizeUseCase()
|
||||
}.execute(cacheSize)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<Async<Unit>>) = launch {
|
||||
suspend {
|
||||
clearCacheUseCase()
|
||||
}.execute(clearCacheAction)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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<FeatureUiModel>,
|
||||
val cacheSize: Async<String>,
|
||||
val clearCacheAction: Async<Unit>,
|
||||
val eventSink: (DeveloperSettingsEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<DeveloperSettingsState> {
|
||||
override val values: Sequence<DeveloperSettingsState>
|
||||
get() = sequenceOf(
|
||||
aDeveloperSettingsState(),
|
||||
aDeveloperSettingsState().copy(clearCacheAction = Async.Loading()),
|
||||
)
|
||||
}
|
||||
|
||||
fun aDeveloperSettingsState() = DeveloperSettingsState(
|
||||
features = aFeatureUiModelList(),
|
||||
cacheSize = Async.Success("1.2 MB"),
|
||||
clearCacheAction = Async.Uninitialized,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<OkHttpClient>,
|
||||
) : 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -14,12 +14,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline
|
||||
package io.element.android.features.preferences.impl.tasks
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeFileSizeFormatter : FileSizeFormatter {
|
||||
override fun format(fileSize: Long): String {
|
||||
return "$fileSize Bytes"
|
||||
class FakeComputeCacheSizeUseCase : ComputeCacheSizeUseCase {
|
||||
override suspend fun invoke() = simulateLongTask {
|
||||
"O kB"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="rageshake_detection_dialog_content">"Zdá sa, že zúrivo trasiete telefónom. Chcete otvoriť obrazovku s nahlásením chýb?"</string>
|
||||
</resources>
|
||||
|
|
@ -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<OkHttpClient>,
|
||||
/*
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bug_report_attach_screenshot">"Priložiť snímku obrazovky"</string>
|
||||
<string name="screen_bug_report_edit_screenshot">"Upraviť snímku obrazovky"</string>
|
||||
<string name="screen_bug_report_editor_placeholder">"Popíšte chybu…"</string>
|
||||
<string name="screen_bug_report_editor_supporting">"Ak je to možné, napíšte popis v angličtine."</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Odoslať snímku obrazovky"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"1 osoba"</item>
|
||||
<item quantity="few">"%1$d ľudia"</item>
|
||||
<item quantity="other">"%1$d ľudí"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_add_topic_title">"Pridať tému"</string>
|
||||
<string name="screen_room_details_edit_room_title">"Upraviť miestnosť"</string>
|
||||
<string name="screen_room_details_edition_error">"Nepodarilo sa nám aktualizovať všetky informácie o tejto miestnosti."</string>
|
||||
<string name="screen_room_details_edition_error_title">"Nepodarilo sa aktualizovať miestnosť"</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Šifrovanie správ je zapnuté"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Pozvať ľudí"</string>
|
||||
<string name="screen_room_details_notification_title">"Oznámenie"</string>
|
||||
<string name="screen_room_details_room_name_label">"Názov miestnosti"</string>
|
||||
<string name="screen_room_details_share_room_title">"Zdieľať miestnosť"</string>
|
||||
<string name="screen_room_details_updating_room">"Aktualizácia miestnosti…"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"Čaká sa"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Členovia miestnosti"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Zablokovať"</string>
|
||||
<string name="screen_dm_details_block_user">"Zablokovať používateľa"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Odblokovať"</string>
|
||||
<string name="screen_dm_details_unblock_user">"Odblokovať používateľa"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Opustiť miestnosť"</string>
|
||||
<string name="screen_room_details_people_title">"Ľudia"</string>
|
||||
<string name="screen_room_details_security_title">"Bezpečnosť"</string>
|
||||
<string name="screen_room_details_topic_title">"Téma"</string>
|
||||
</resources>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_roomlist_a11y_create_message">"Vytvorte novú konverzáciu alebo miestnosť"</string>
|
||||
<string name="screen_roomlist_main_space_title">"Všetky konverzácie"</string>
|
||||
<string name="session_verification_banner_message">"Vyzerá to tak, že používate nové zariadenie. Overte, či ste to vy, aby ste mali prístup k zašifrovaným správam."</string>
|
||||
<string name="session_verification_banner_title">"Získajte prístup k histórii vašich správ"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_session_verification_cancelled_subtitle">"Zdá sa, že niečo nie je v poriadku. Časový limit žiadosti vypršal alebo bola žiadosť zamietnutá."</string>
|
||||
<string name="screen_session_verification_compare_emojis_subtitle">"Skontrolujte, či sa emotikony uvedené nižšie zhodujú s emotikonmi zobrazenými vo vašej druhej relácii."</string>
|
||||
<string name="screen_session_verification_compare_emojis_title">"Porovnajte emotikony"</string>
|
||||
<string name="screen_session_verification_complete_subtitle">"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ú."</string>
|
||||
<string name="screen_session_verification_open_existing_session_subtitle">"Dokážte, že ste to vy, aby ste získali prístup k histórii vašich zašifrovaných správ."</string>
|
||||
<string name="screen_session_verification_open_existing_session_title">"Otvoriť existujúcu reláciu"</string>
|
||||
<string name="screen_session_verification_positive_button_canceled">"Zopakovať overenie"</string>
|
||||
<string name="screen_session_verification_positive_button_initial">"Som pripravený/á"</string>
|
||||
<string name="screen_session_verification_positive_button_verifying_ongoing">"Čaká sa na zhodu"</string>
|
||||
<string name="screen_session_verification_request_accepted_subtitle">"Porovnajte jedinečné emotikony a uistite sa, že sú zobrazené v rovnakom poradí."</string>
|
||||
<string name="screen_session_verification_they_dont_match">"Nezhodujú sa"</string>
|
||||
<string name="screen_session_verification_they_match">"Zhodujú sa"</string>
|
||||
<string name="screen_session_verification_waiting_to_accept_subtitle">"Ak chcete pokračovať, prijmite žiadosť o spustenie procesu overenia vo vašej druhej relácii."</string>
|
||||
<string name="screen_session_verification_waiting_to_accept_title">"Čaká sa na prijatie žiadosti"</string>
|
||||
<string name="screen_session_verification_cancelled_title">"Overovanie zrušené"</string>
|
||||
<string name="screen_session_verification_positive_button_ready">"Spustiť"</string>
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue