Merge branch 'develop' into feature/fga/knock_requests_sdk
This commit is contained in:
commit
5275a3e5d3
391 changed files with 6199 additions and 1991 deletions
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.0.21" />
|
||||
<option name="version" value="2.1.0" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -30,5 +30,6 @@ appId: ${MAESTRO_APP_ID}
|
|||
# assert there's 1 member and 2 invitees
|
||||
- tapOn: "Back"
|
||||
- scroll
|
||||
- scroll
|
||||
- tapOn: "Leave room"
|
||||
- tapOn: "Leave"
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ allprojects {
|
|||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.19")
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.22")
|
||||
}
|
||||
|
||||
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
|
||||
|
|
|
|||
|
|
@ -130,9 +130,9 @@ Prerequisites:
|
|||
```
|
||||
cargo install cargo-ndk
|
||||
```
|
||||
* Install the Android Rust toolchain:
|
||||
* Install the Android Rust toolchain for your machine's hardware:
|
||||
```
|
||||
rustup target add aarch64-linux-android
|
||||
rustup target add aarch64-linux-android x86_64-linux-android
|
||||
```
|
||||
* Depending on the location of the Android SDK, you may need to set
|
||||
`ANDROID_HOME`:
|
||||
|
|
|
|||
|
|
@ -4,15 +4,26 @@
|
|||
<string name="screen_knock_requests_list_accept_all_alert_description">"Opravdu chcete přijmout všechny žádosti o vstup?"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_title">"Přijmout všechny požadavky"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_button_title">"Přijmout vše"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"Nemohli jsme přijmout všechny žádosti. Chcete to zkusit znovu?"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Nepodařilo se přijmout všechny žádosti"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_loading_title">"Přijímání všech žádostí o vstup"</string>
|
||||
<string name="screen_knock_requests_list_accept_failed_alert_description">"Tuto žádost jsme nemohli přijmout. Chcete to zkusit znovu?"</string>
|
||||
<string name="screen_knock_requests_list_accept_failed_alert_title">"Žádost se nepodařilo přijmout"</string>
|
||||
<string name="screen_knock_requests_list_accept_loading_title">"Přijímání žádosti o vstup"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Ano, odmítnout a vykázat"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_description">"Opravdu chcete odmítnout a vykázat %1$s? Tento uživatel nebude moci znovu požádat o vstup do této místnosti."</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_title">"Odmítnout a zakázat vstup"</string>
|
||||
<string name="screen_knock_requests_list_ban_loading_title">"Odmítání vstupu a vykázání"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Ano, odmítnout"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_description">"Opravdu chcete odmítnout %1$s žádost o vstup do této místnosti?"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_title">"Odmítnout vstup"</string>
|
||||
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Odmítnout a vykázat"</string>
|
||||
<string name="screen_knock_requests_list_decline_failed_alert_description">"Tuto žádost jsme nemohli odmítnout. Chcete to zkusit znovu?"</string>
|
||||
<string name="screen_knock_requests_list_decline_failed_alert_title">"Žádost se nepodařilo odmítnout"</string>
|
||||
<string name="screen_knock_requests_list_decline_loading_title">"Odmítání žádosti o vstup"</string>
|
||||
<string name="screen_knock_requests_list_empty_state_description">"Když někdo požádá o vstup do místnosti, uvidíte jeho žádost zde."</string>
|
||||
<string name="screen_knock_requests_list_empty_state_title">"Žádná čekající žádost o vstup"</string>
|
||||
<string name="screen_knock_requests_list_initial_loading_title">"Načítání žádostí o vstup…"</string>
|
||||
<string name="screen_knock_requests_list_title">"Žádosti o vstup"</string>
|
||||
<plurals name="screen_room_multiple_knock_requests_title">
|
||||
<item quantity="one">"%1$s +%2$d další chce vstoupit do této místnosti"</item>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,17 @@
|
|||
<string name="screen_knock_requests_list_accept_all_alert_description">"Σίγουρα θες να αποδεχτείς όλα τα αιτήματα συμμετοχής;"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_title">"Αποδοχή όλων των αιτημάτων"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_button_title">"Αποδοχή όλων"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_loading_title">"Αποδοχή όλων των αιτημάτων συμμετοχής"</string>
|
||||
<string name="screen_knock_requests_list_accept_loading_title">"Γίνεται αποδοχή αιτήματος συμμετοχής"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Ναι, απόρριψη και αποκλεισμός"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_description">"Σίγουρα θες να απορρίψειε και να αποκλείσεις τον χρήστη %1$s; Αυτός ο χρήστης δεν θα μπορεί να ζητήσει πρόσβαση για να συμμετάσχει ξανά σε αυτό το δωμάτιο."</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_title">"Απόρριψη και αποκλεισμός πρόσβασης"</string>
|
||||
<string name="screen_knock_requests_list_ban_loading_title">"Γίνεται απόρριψη και αποκλεισμός πρόσβασης"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Ναι, απόρριψη"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_description">"Σίγουρα θες να απορρίψεις το αίτημα του χρήστη %1$s να συμμετάσχει στο δωμάτιο;"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_title">"Απόρριψη πρόσβασης"</string>
|
||||
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Απόρριψη και αποκλεισμός"</string>
|
||||
<string name="screen_knock_requests_list_decline_loading_title">"Γίνεται απόρριψη αιτήματος συμμετοχής"</string>
|
||||
<string name="screen_knock_requests_list_empty_state_description">"Όταν κάποιος θα ζητήσει να συμμετάσχει στο δωμάτιο, θα μπορείς να δεις το αίτημά του εδώ."</string>
|
||||
<string name="screen_knock_requests_list_empty_state_title">"Δεν υπάρχει εκκρεμές αίτημα συμμετοχής"</string>
|
||||
<string name="screen_knock_requests_list_title">"Αιτήματα συμμετοχής"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_knock_requests_list_accept_all_alert_confirm_button_title">"Kyllä, hyväksy kaikki"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_description">"Haluatko varmasti hyväksyä kaikki liittymispyynnöt?"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_title">"Hyväksy kaikki pyynnöt"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_button_title">"Hyväksy kaikki"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Kyllä, hylkää ja anna porttikielto"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_description">"Haluatko varmasti hylätä käyttäjän %1$s pyynnön liittyä huoneeseen ja antaa hänelle porttikiellon? Hän ei voi enää pyytää lupaa liittyä tähän huoneeseen."</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_title">"Hylkää ja anna porttikielto"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Kyllä, hylkää"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_description">"Haluatko varmasti hylätä käyttäjän %1$s pyynnön liittyä tähän huoneeseen?"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_title">"Hylkää pyyntö"</string>
|
||||
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Hylkää ja anna porttikielto"</string>
|
||||
<string name="screen_knock_requests_list_empty_state_description">"Kun joku pyytää liittyä huoneeseen, näet hänen pyyntönsä täällä."</string>
|
||||
<string name="screen_knock_requests_list_empty_state_title">"Ei odottavia liittymispyyntöjä"</string>
|
||||
<string name="screen_knock_requests_list_title">"Liittymispyynnöt"</string>
|
||||
<plurals name="screen_room_multiple_knock_requests_title">
|
||||
<item quantity="one">"%1$s +%2$d muu haluavat liittyä tähän huoneeseen"</item>
|
||||
<item quantity="other">"%1$s +%2$d muuta haluavat liittyä tähän huoneeseen"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Näytä kaikki"</string>
|
||||
<string name="screen_room_single_knock_request_accept_button_title">"Hyväksy"</string>
|
||||
<string name="screen_room_single_knock_request_title">"%1$s haluaa liittyä tähän huoneeseen"</string>
|
||||
<string name="screen_room_single_knock_request_view_button_title">"Näytä"</string>
|
||||
</resources>
|
||||
|
|
@ -4,15 +4,26 @@
|
|||
<string name="screen_knock_requests_list_accept_all_alert_description">"Êtes-vous sûr de vouloir accepter toutes les demandes pour rejoindre le salon ?"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_title">"Tout accepter"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_button_title">"Tout accepter"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"Toutes les demandes n’ont pas pu être acceptées. Voulez-vous réessayer ?"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Toutes les demandes n’ont pas été acceptées"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_loading_title">"Accepter toutes les demandes à rejoindre"</string>
|
||||
<string name="screen_knock_requests_list_accept_failed_alert_description">"La demande n’a pas pu être acceptée. Voulez-vous réessayer ?"</string>
|
||||
<string name="screen_knock_requests_list_accept_failed_alert_title">"Impossible d’accepter la demande"</string>
|
||||
<string name="screen_knock_requests_list_accept_loading_title">"Accepter la demande à rejoindre"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Oui, rejeter et bannir"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_description">"Êtes-vous sûr de vouloir rejeter la demande et bannir %1$s ? Cet utilisateur ne pourra pas demander à nouveau à rejoindre ce salon."</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_title">"Refuser et interdire l’accès"</string>
|
||||
<string name="screen_knock_requests_list_ban_loading_title">"En cours de traitement…"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Oui, refuser"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_description">"Êtes-vous sûr de vouloir refuser la demande de %1$s à rejoindre le salon ?"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_title">"Refuser l’accès"</string>
|
||||
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Refuser et bannir"</string>
|
||||
<string name="screen_knock_requests_list_decline_failed_alert_description">"Nous n’avons pas pu refuser cette demande. Voulez-vous réessayer ?"</string>
|
||||
<string name="screen_knock_requests_list_decline_failed_alert_title">"Echec"</string>
|
||||
<string name="screen_knock_requests_list_decline_loading_title">"Traitement en cours…"</string>
|
||||
<string name="screen_knock_requests_list_empty_state_description">"Lorsque quelqu’un demandera à rejoindre le salon, vous pourrez voir sa demande ici."</string>
|
||||
<string name="screen_knock_requests_list_empty_state_title">"Personne ne demande à rejoindre le salon"</string>
|
||||
<string name="screen_knock_requests_list_initial_loading_title">"Chargement…"</string>
|
||||
<string name="screen_knock_requests_list_title">"Demandes en attente"</string>
|
||||
<plurals name="screen_room_multiple_knock_requests_title">
|
||||
<item quantity="one">"%1$s et %2$d autre personne souhaitent rejoindre ce salon"</item>
|
||||
|
|
|
|||
|
|
@ -4,15 +4,26 @@
|
|||
<string name="screen_knock_requests_list_accept_all_alert_description">"Biztos, hogy elfogadja az összes csatlakozási kérelmet?"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_title">"Minden kérés elfogadása"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_button_title">"Összes elfogadása"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"Nem sikerült az összes kérés fogadása. Újra megpróbálja?"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Nem sikerült az összes kérés elfogadása"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_loading_title">"Összes csatlakozási kérés elfogadása"</string>
|
||||
<string name="screen_knock_requests_list_accept_failed_alert_description">"Nem sikerült elfogadni a kérést. Megpróbálja újra?"</string>
|
||||
<string name="screen_knock_requests_list_accept_failed_alert_title">"Nem sikerült elfogadni a kérést"</string>
|
||||
<string name="screen_knock_requests_list_accept_loading_title">"Csatlakozási kérés elfogadása"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Igen, elutasítás és kitiltás"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_description">"Biztos, hogy elutasítja %1$s kérését és ki is tiltja? Többé nem fogja tudni azt kérni, hogy csatlakozhasson ehhez a szobához."</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_title">"A hozzáférés elutasítása és kitiltás"</string>
|
||||
<string name="screen_knock_requests_list_ban_loading_title">"A hozzáférés megtagadása és kitiltás"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Igen, elutasítás"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_description">"Biztos, hogy elutasítja %1$s kérését, hogy csatlakozzon a szobához?"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_title">"Hozzáférés elutasítása"</string>
|
||||
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Elutasítás és kitiltás"</string>
|
||||
<string name="screen_knock_requests_list_decline_failed_alert_description">"Nem sikerült elutasítani a kérést. Megpróbálja újra?"</string>
|
||||
<string name="screen_knock_requests_list_decline_failed_alert_title">"Nem sikerült elutasítani a kérést"</string>
|
||||
<string name="screen_knock_requests_list_decline_loading_title">"Csatlakozási kérés elutasítása"</string>
|
||||
<string name="screen_knock_requests_list_empty_state_description">"Ha valaki csatlakozni kíván a szobához, itt láthatja a kérését."</string>
|
||||
<string name="screen_knock_requests_list_empty_state_title">"Nincs függőben lévő csatlakozási kérelem"</string>
|
||||
<string name="screen_knock_requests_list_initial_loading_title">"Csatlakozási kérések betöltése…"</string>
|
||||
<string name="screen_knock_requests_list_title">"Csatlakozási kérelmek"</string>
|
||||
<plurals name="screen_room_multiple_knock_requests_title">
|
||||
<item quantity="one">"%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához"</item>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.licenses.impl.list
|
||||
|
||||
sealed interface DependencyLicensesListEvent {
|
||||
data class SetFilter(val filter: String) : DependencyLicensesListEvent
|
||||
}
|
||||
|
|
@ -29,6 +29,10 @@ class DependencyLicensesListPresenter @Inject constructor(
|
|||
var licenses by remember {
|
||||
mutableStateOf<AsyncData<ImmutableList<DependencyLicenseItem>>>(AsyncData.Loading())
|
||||
}
|
||||
var filteredLicenses by remember {
|
||||
mutableStateOf<AsyncData<ImmutableList<DependencyLicenseItem>>>(AsyncData.Loading())
|
||||
}
|
||||
var filter by remember { mutableStateOf("") }
|
||||
LaunchedEffect(Unit) {
|
||||
runCatching {
|
||||
licenses = AsyncData.Success(licensesProvider.provides().toPersistentList())
|
||||
|
|
@ -36,6 +40,32 @@ class DependencyLicensesListPresenter @Inject constructor(
|
|||
licenses = AsyncData.Failure(it)
|
||||
}
|
||||
}
|
||||
return DependencyLicensesListState(licenses = licenses)
|
||||
LaunchedEffect(filter, licenses.dataOrNull()) {
|
||||
val data = licenses.dataOrNull()
|
||||
val safeFilter = filter.trim()
|
||||
if (data != null && safeFilter.isNotEmpty()) {
|
||||
filteredLicenses = AsyncData.Success(data.filter {
|
||||
it.safeName.contains(safeFilter, ignoreCase = true) ||
|
||||
it.groupId.contains(safeFilter, ignoreCase = true) ||
|
||||
it.artifactId.contains(safeFilter, ignoreCase = true)
|
||||
}.toPersistentList())
|
||||
} else {
|
||||
filteredLicenses = licenses
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvent(dependencyLicensesListEvent: DependencyLicensesListEvent) {
|
||||
when (dependencyLicensesListEvent) {
|
||||
is DependencyLicensesListEvent.SetFilter -> {
|
||||
filter = dependencyLicensesListEvent.filter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DependencyLicensesListState(
|
||||
licenses = filteredLicenses,
|
||||
filter = filter,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,6 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
|
||||
data class DependencyLicensesListState(
|
||||
val licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
|
||||
val filter: String,
|
||||
val eventSink: (DependencyLicensesListEvent) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,28 +11,49 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.features.licenses.impl.model.License
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class DependencyLicensesListStateProvider : PreviewParameterProvider<DependencyLicensesListState> {
|
||||
override val values: Sequence<DependencyLicensesListState>
|
||||
get() = sequenceOf(
|
||||
DependencyLicensesListState(
|
||||
aDependencyLicensesListState(
|
||||
licenses = AsyncData.Loading()
|
||||
),
|
||||
DependencyLicensesListState(
|
||||
aDependencyLicensesListState(
|
||||
licenses = AsyncData.Failure(Exception("Failed to load licenses"))
|
||||
),
|
||||
DependencyLicensesListState(
|
||||
aDependencyLicensesListState(
|
||||
licenses = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aDependencyLicenseItem(),
|
||||
aDependencyLicenseItem(name = null),
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
aDependencyLicensesListState(
|
||||
licenses = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aDependencyLicenseItem(),
|
||||
aDependencyLicenseItem(name = null),
|
||||
)
|
||||
),
|
||||
filter = "a filter",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun aDependencyLicensesListState(
|
||||
licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
|
||||
filter: String = "",
|
||||
): DependencyLicensesListState {
|
||||
return DependencyLicensesListState(
|
||||
licenses = licenses,
|
||||
filter = filter,
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aDependencyLicenseItem(
|
||||
name: String? = "A dependency",
|
||||
) = DependencyLicenseItem(
|
||||
|
|
|
|||
|
|
@ -7,31 +7,36 @@
|
|||
|
||||
package io.element.android.features.licenses.impl.list
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
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.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun DependencyLicensesListView(
|
||||
state: DependencyLicensesListState,
|
||||
|
|
@ -48,48 +53,64 @@ fun DependencyLicensesListView(
|
|||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
LazyColumn(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
when (state.licenses) {
|
||||
is AsyncData.Failure -> item {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.common_error),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
AsyncData.Uninitialized,
|
||||
is AsyncData.Loading -> item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 64.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
if (state.licenses.isSuccess()) {
|
||||
// Search field
|
||||
OutlinedTextField(
|
||||
value = state.filter,
|
||||
onValueChange = { state.eventSink(DependencyLicensesListEvent.SetFilter(it)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Search(),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
LazyColumn {
|
||||
when (state.licenses) {
|
||||
is AsyncData.Failure -> item {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.common_error),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
is AsyncData.Success -> items(state.licenses.data) { license ->
|
||||
ListItem(
|
||||
headlineContent = { Text(license.safeName) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
buildString {
|
||||
append(license.groupId)
|
||||
append(":")
|
||||
append(license.artifactId)
|
||||
append(":")
|
||||
append(license.version)
|
||||
}
|
||||
AsyncData.Uninitialized,
|
||||
is AsyncData.Loading -> item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 64.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onOpenLicense(license)
|
||||
}
|
||||
)
|
||||
}
|
||||
is AsyncData.Success -> items(state.licenses.data) { license ->
|
||||
ListItem(
|
||||
headlineContent = { Text(license.safeName) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
buildString {
|
||||
append(license.groupId)
|
||||
append(":")
|
||||
append(license.artifactId)
|
||||
append(":")
|
||||
append(license.version)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onOpenLicense(license)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class DependencyLicensesListPresenterTest {
|
|||
val finalState = awaitItem()
|
||||
assertThat(finalState.licenses.isSuccess()).isTrue()
|
||||
assertThat(finalState.licenses.dataOrNull()).isEmpty()
|
||||
assertThat(finalState.filter).isEqualTo("")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +55,40 @@ class DependencyLicensesListPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state, one license, set filter`() = runTest {
|
||||
val anItem = aDependencyLicenseItem()
|
||||
val presenter = createPresenter {
|
||||
listOf(anItem)
|
||||
}
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.licenses).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val loadedState = awaitItem()
|
||||
assertThat(loadedState.licenses.isSuccess()).isTrue()
|
||||
assertThat(loadedState.licenses.dataOrNull()!!.size).isEqualTo(1)
|
||||
loadedState.eventSink(DependencyLicensesListEvent.SetFilter("dep"))
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(1)
|
||||
assertThat(state.filter).isEqualTo("dep")
|
||||
}
|
||||
loadedState.eventSink(DependencyLicensesListEvent.SetFilter("bleh"))
|
||||
skipItems(1)
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(0)
|
||||
assertThat(state.filter).isEqualTo("bleh")
|
||||
}
|
||||
loadedState.eventSink(DependencyLicensesListEvent.SetFilter(""))
|
||||
skipItems(1)
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(1)
|
||||
assertThat(state.filter).isEqualTo("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
provideResult: () -> List<DependencyLicenseItem>
|
||||
) = DependencyLicensesListPresenter(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<string name="screen_app_lock_biometric_authentication">"biometrinen tunnistus"</string>
|
||||
<string name="screen_app_lock_biometric_unlock">"biometrinen tunnistus"</string>
|
||||
<string name="screen_app_lock_biometric_unlock_title_android">"Avaa biometrisellä"</string>
|
||||
<string name="screen_app_lock_confirm_biometric_authentication_android">"Vahvista biometrinen tunniste"</string>
|
||||
<string name="screen_app_lock_forgot_pin">"Unohtuiko PIN-koodi?"</string>
|
||||
<string name="screen_app_lock_settings_change_pin">"Vaihda PIN-koodi"</string>
|
||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Salli biometrinen tunnistus"</string>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ dependencies {
|
|||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.roomselect.api)
|
||||
implementation(projects.libraries.voiceplayer.api)
|
||||
implementation(projects.libraries.voicerecorder.api)
|
||||
implementation(projects.libraries.mediaplayer.api)
|
||||
implementation(projects.libraries.uiUtils)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||
import io.element.android.features.poll.api.create.CreatePollMode
|
||||
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
||||
|
|
@ -55,6 +56,8 @@ import io.element.android.libraries.architecture.createNode
|
|||
import io.element.android.libraries.architecture.overlay.Overlay
|
||||
import io.element.android.libraries.architecture.overlay.operation.hide
|
||||
import io.element.android.libraries.architecture.overlay.operation.show
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
|
@ -97,6 +100,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
|
||||
private val timelineController: TimelineController,
|
||||
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
||||
private val dateFormatter: DateFormatter,
|
||||
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
|
||||
|
|
@ -436,7 +440,15 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
senderId = event.senderId,
|
||||
senderName = event.safeSenderName,
|
||||
senderAvatar = event.senderAvatar.url,
|
||||
dateSent = event.sentTime,
|
||||
dateSent = dateFormatter.format(
|
||||
event.sentTimeMillis,
|
||||
mode = DateFormatterMode.Day,
|
||||
),
|
||||
dateSentFull = dateFormatter.format(
|
||||
timestamp = event.sentTimeMillis,
|
||||
mode = DateFormatterMode.Full,
|
||||
),
|
||||
waveform = (content as? TimelineItemVoiceContent)?.waveform,
|
||||
),
|
||||
mediaSource = mediaSource,
|
||||
thumbnailSource = thumbnailSource,
|
||||
|
|
|
|||
|
|
@ -274,7 +274,8 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
TimelineItemAction.CopyCaption -> handleCopyCaption(targetEvent)
|
||||
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
|
||||
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
||||
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.EditPoll -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
||||
TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState)
|
||||
TimelineItemAction.EditCaption -> handleActionEditCaption(targetEvent, composerState)
|
||||
TimelineItemAction.RemoveCaption -> handleRemoveCaption(targetEvent)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ import io.element.android.features.messages.impl.timeline.model.event.canBeCopie
|
|||
import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded
|
||||
import io.element.android.features.messages.impl.timeline.model.event.canReact
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
|
|
@ -64,6 +66,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
|||
private val room: MatrixRoom,
|
||||
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val dateFormatter: DateFormatter,
|
||||
) : ActionListPresenter {
|
||||
@AssistedFactory
|
||||
@ContributesBinding(RoomScope::class)
|
||||
|
|
@ -131,6 +134,11 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
|||
if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != VerifiedUserSendFailure.None) {
|
||||
target.value = ActionListState.Target.Success(
|
||||
event = timelineItem,
|
||||
sentTimeFull = dateFormatter.format(
|
||||
timelineItem.sentTimeMillis,
|
||||
DateFormatterMode.Full,
|
||||
useRelative = true,
|
||||
),
|
||||
displayEmojiReactions = displayEmojiReactions,
|
||||
verifiedUserSendFailure = verifiedUserSendFailure,
|
||||
actions = actions.toImmutableList()
|
||||
|
|
@ -170,6 +178,8 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
|||
add(TimelineItemAction.EditCaption)
|
||||
add(TimelineItemAction.RemoveCaption)
|
||||
}
|
||||
} else if (timelineItem.content is TimelineItemPollContent) {
|
||||
add(TimelineItemAction.EditPoll)
|
||||
} else {
|
||||
add(TimelineItemAction.Edit)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ data class ActionListState(
|
|||
data class Loading(val event: TimelineItem.Event) : Target
|
||||
data class Success(
|
||||
val event: TimelineItem.Event,
|
||||
val sentTimeFull: String,
|
||||
val displayEmojiReactions: Boolean,
|
||||
val verifiedUserSendFailure: VerifiedUserSendFailure,
|
||||
val actions: ImmutableList<TimelineItemAction>,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
event = aTimelineItemEvent(
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
|
|
@ -49,6 +50,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
displayNameAmbiguous = true,
|
||||
timelineItemReactions = reactionsState,
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(
|
||||
|
|
@ -62,6 +64,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
content = aTimelineItemVideoContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(
|
||||
|
|
@ -75,6 +78,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
content = aTimelineItemFileContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(
|
||||
|
|
@ -88,6 +92,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
content = aTimelineItemAudioContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(
|
||||
|
|
@ -101,6 +106,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
content = aTimelineItemVoiceContent(caption = null),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(
|
||||
|
|
@ -114,6 +120,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
content = aTimelineItemLocationContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
|
|
@ -125,6 +132,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
content = aTimelineItemLocationContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = false,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
|
|
@ -136,6 +144,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
content = aTimelineItemPollContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = false,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemPollActionList(),
|
||||
|
|
@ -147,6 +156,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
timelineItemReactions = reactionsState,
|
||||
messageShield = MessageShield.UnknownDevice(isCritical = true)
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
|
|
@ -155,6 +165,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
|
||||
actions = aTimelineItemActionList(),
|
||||
|
|
@ -192,6 +203,7 @@ fun aTimelineItemActionList(
|
|||
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
|
||||
return setOf(
|
||||
TimelineItemAction.EndPoll,
|
||||
TimelineItemAction.EditPoll,
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
|
|
|
|||
|
|
@ -185,6 +185,7 @@ private fun ActionListViewContent(
|
|||
Column {
|
||||
MessageSummary(
|
||||
event = target.event,
|
||||
sentTimeFull = target.sentTimeFull,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
|
|
@ -245,7 +246,11 @@ private fun ActionListViewContent(
|
|||
|
||||
@Suppress("MultipleEmitters") // False positive
|
||||
@Composable
|
||||
private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) {
|
||||
private fun MessageSummary(
|
||||
event: TimelineItem.Event,
|
||||
sentTimeFull: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val content: @Composable () -> Unit
|
||||
val icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.MessageActionSender)) }
|
||||
val contentStyle = ElementTheme.typography.fontBodyMdRegular.copy(color = MaterialTheme.colorScheme.secondary)
|
||||
|
|
@ -300,20 +305,23 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
|||
icon()
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
SenderName(
|
||||
senderId = event.senderId,
|
||||
senderProfile = event.senderProfile,
|
||||
senderNameMode = SenderNameMode.ActionList,
|
||||
)
|
||||
Row {
|
||||
SenderName(
|
||||
modifier = Modifier.weight(1f),
|
||||
senderId = event.senderId,
|
||||
senderProfile = event.senderProfile,
|
||||
senderNameMode = SenderNameMode.ActionList,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
text = sentTimeFull,
|
||||
style = ElementTheme.typography.fontBodyXsRegular,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
}
|
||||
content()
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
event.sentTime,
|
||||
style = ElementTheme.typography.fontBodyXsRegular,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,30 +11,30 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Immutable
|
||||
sealed class TimelineItemAction(
|
||||
enum class TimelineItemAction(
|
||||
@StringRes val titleRes: Int,
|
||||
@DrawableRes val icon: Int,
|
||||
val destructive: Boolean = false
|
||||
) {
|
||||
data object ViewInTimeline : TimelineItemAction(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on)
|
||||
data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward)
|
||||
data object CopyText : TimelineItemAction(CommonStrings.action_copy_text, CompoundDrawables.ic_compound_copy)
|
||||
data object CopyCaption : TimelineItemAction(CommonStrings.action_copy_caption, CompoundDrawables.ic_compound_copy)
|
||||
data object CopyLink : TimelineItemAction(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link)
|
||||
data object Redact : TimelineItemAction(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true)
|
||||
data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply)
|
||||
data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply)
|
||||
data object Edit : TimelineItemAction(CommonStrings.action_edit, CompoundDrawables.ic_compound_edit)
|
||||
data object EditCaption : TimelineItemAction(CommonStrings.action_edit_caption, CompoundDrawables.ic_compound_edit)
|
||||
data object AddCaption : TimelineItemAction(CommonStrings.action_add_caption, CompoundDrawables.ic_compound_edit)
|
||||
data object RemoveCaption : TimelineItemAction(CommonStrings.action_remove_caption, CompoundDrawables.ic_compound_delete, destructive = true)
|
||||
data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_developer_options)
|
||||
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true)
|
||||
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end)
|
||||
data object Pin : TimelineItemAction(CommonStrings.action_pin, CompoundDrawables.ic_compound_pin)
|
||||
data object Unpin : TimelineItemAction(CommonStrings.action_unpin, CompoundDrawables.ic_compound_unpin)
|
||||
ViewInTimeline(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on),
|
||||
Forward(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward),
|
||||
CopyText(CommonStrings.action_copy_text, CompoundDrawables.ic_compound_copy),
|
||||
CopyCaption(CommonStrings.action_copy_caption, CompoundDrawables.ic_compound_copy),
|
||||
CopyLink(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link),
|
||||
Redact(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true),
|
||||
Reply(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply),
|
||||
ReplyInThread(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply),
|
||||
Edit(CommonStrings.action_edit, CompoundDrawables.ic_compound_edit),
|
||||
EditPoll(CommonStrings.action_edit_poll, CompoundDrawables.ic_compound_edit),
|
||||
EditCaption(CommonStrings.action_edit_caption, CompoundDrawables.ic_compound_edit),
|
||||
AddCaption(CommonStrings.action_add_caption, CompoundDrawables.ic_compound_edit),
|
||||
RemoveCaption(CommonStrings.action_remove_caption, CompoundDrawables.ic_compound_close, destructive = true),
|
||||
ViewSource(CommonStrings.action_view_source, CompoundDrawables.ic_compound_code),
|
||||
ReportContent(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true),
|
||||
EndPoll(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end),
|
||||
Pin(CommonStrings.action_pin, CompoundDrawables.ic_compound_pin),
|
||||
Unpin(CommonStrings.action_unpin, CompoundDrawables.ic_compound_unpin),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,21 +7,25 @@
|
|||
|
||||
package io.element.android.features.messages.impl.actionlist.model
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
|
||||
class TimelineItemActionComparator : Comparator<TimelineItemAction> {
|
||||
// See order in https://www.figma.com/design/ux3tYoZV9WghC7hHT9Fhk0/Compound-iOS-Components?node-id=2946-2392
|
||||
private val orderedList = listOf(
|
||||
@VisibleForTesting
|
||||
val orderedList = listOf(
|
||||
TimelineItemAction.EndPoll,
|
||||
TimelineItemAction.ViewInTimeline,
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.ReplyInThread,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.Unpin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.EditPoll,
|
||||
TimelineItemAction.AddCaption,
|
||||
TimelineItemAction.EditCaption,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.Unpin,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.CopyCaption,
|
||||
TimelineItemAction.RemoveCaption,
|
||||
TimelineItemAction.ViewSource,
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ class PinnedMessagesListTimelineActionPostProcessor : TimelineItemActionPostProc
|
|||
override fun process(actions: List<TimelineItemAction>): List<TimelineItemAction> {
|
||||
return buildList {
|
||||
add(TimelineItemAction.ViewInTimeline)
|
||||
actions.firstOrNull { it is TimelineItemAction.Unpin }?.let(::add)
|
||||
actions.firstOrNull { it is TimelineItemAction.Forward }?.let(::add)
|
||||
actions.firstOrNull { it is TimelineItemAction.ViewSource }?.let(::add)
|
||||
actions.firstOrNull { it == TimelineItemAction.Unpin }?.let(::add)
|
||||
actions.firstOrNull { it == TimelineItemAction.Forward }?.let(::add)
|
||||
actions.firstOrNull { it == TimelineItemAction.ViewSource }?.let(::add)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,9 +40,12 @@ fun TimelineItemEncryptedView(
|
|||
UtdCause.UnknownDevice -> {
|
||||
CommonStrings.common_unable_to_decrypt_insecure_device to CompoundDrawables.ic_compound_block
|
||||
}
|
||||
UtdCause.HistoricalMessage -> {
|
||||
UtdCause.HistoricalMessageAndBackupIsDisabled -> {
|
||||
CommonStrings.timeline_decryption_failure_historical_event_no_key_backup to CompoundDrawables.ic_compound_block
|
||||
}
|
||||
UtdCause.HistoricalMessageAndDeviceIsUnverified -> {
|
||||
CommonStrings.timeline_decryption_failure_historical_event_unverified_device to CompoundDrawables.ic_compound_block
|
||||
}
|
||||
UtdCause.WithheldUnverifiedOrInsecureDevice -> {
|
||||
CommonStrings.timeline_decryption_failure_withheld_unverified to CompoundDrawables.ic_compound_block
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,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.model.event.TimelineItemVoiceContent
|
||||
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||
|
||||
@Composable
|
||||
fun TimelineItemEventContentView(
|
||||
|
|
|
|||
|
|
@ -40,9 +40,6 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
|||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentProvider
|
||||
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageEvents
|
||||
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
|
||||
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageStateProvider
|
||||
import io.element.android.libraries.androidutils.accessibility.isScreenReaderEnabled
|
||||
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -52,6 +49,9 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@
|
|||
package io.element.android.features.messages.impl.timeline.di
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
|
||||
import io.element.android.features.messages.impl.voicemessages.timeline.aVoiceMessageState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||
import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
|
||||
|
||||
/**
|
||||
* A fake [TimelineItemPresenterFactories] for screenshot tests.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou
|
|||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
||||
import io.element.android.libraries.core.bool.orTrue
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
|
@ -32,14 +33,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getDisambigua
|
|||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
class TimelineItemEventFactory @AssistedInject constructor(
|
||||
@Assisted private val config: TimelineItemsFactoryConfig,
|
||||
private val contentFactory: TimelineItemContentFactory,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
|
||||
private val dateFormatter: DateFormatter,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
) {
|
||||
@AssistedFactory
|
||||
|
|
@ -57,9 +57,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
|
|||
val groupPosition =
|
||||
computeGroupPosition(currentTimelineItem, timelineItems, index)
|
||||
val senderProfile = currentTimelineItem.event.senderProfile
|
||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp))
|
||||
|
||||
val sentTime = dateFormatter.format(
|
||||
timestamp = currentTimelineItem.event.timestamp,
|
||||
mode = DateFormatterMode.TimeOnly,
|
||||
)
|
||||
val senderAvatarData = AvatarData(
|
||||
id = currentSender.value,
|
||||
name = senderProfile.getDisambiguatedDisplayName(currentSender),
|
||||
|
|
@ -78,6 +79,7 @@ class TimelineItemEventFactory @AssistedInject constructor(
|
|||
isMine = currentTimelineItem.event.isOwn,
|
||||
isEditable = currentTimelineItem.event.isEditable,
|
||||
canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo,
|
||||
sentTimeMillis = currentTimelineItem.event.timestamp,
|
||||
sentTime = sentTime,
|
||||
groupPosition = groupPosition,
|
||||
reactionsState = currentTimelineItem.computeReactionsState(),
|
||||
|
|
@ -106,7 +108,6 @@ class TimelineItemEventFactory @AssistedInject constructor(
|
|||
if (!config.computeReactions) {
|
||||
return TimelineItemReactions(reactions = persistentListOf())
|
||||
}
|
||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
var aggregatedReactions = this.event.reactions.map { reaction ->
|
||||
// Sort reactions within an aggregation by timestamp descending.
|
||||
// This puts the most recent at the top, useful in cases like the
|
||||
|
|
@ -121,7 +122,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
|
|||
AggregatedReactionSender(
|
||||
senderId = it.senderId,
|
||||
timestamp = date,
|
||||
sentTime = timeFormatter.format(date),
|
||||
sentTime = dateFormatter.format(
|
||||
it.timestamp,
|
||||
DateFormatterMode.TimeOrDate,
|
||||
),
|
||||
)
|
||||
}
|
||||
.toImmutableList()
|
||||
|
|
@ -157,7 +161,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
|
|||
url = roomMember?.avatarUrl,
|
||||
size = AvatarSize.TimelineReadReceipt,
|
||||
),
|
||||
formattedDate = lastMessageTimestampFormatter.format(receipt.timestamp)
|
||||
formattedDate = dateFormatter.format(
|
||||
receipt.timestamp,
|
||||
mode = DateFormatterMode.TimeOrDate,
|
||||
)
|
||||
)
|
||||
}
|
||||
.toImmutableList()
|
||||
|
|
|
|||
|
|
@ -9,13 +9,20 @@ package io.element.android.features.messages.impl.timeline.factories.virtual
|
|||
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
|
||||
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemDaySeparatorFactory @Inject constructor(private val daySeparatorFormatter: DaySeparatorFormatter) {
|
||||
class TimelineItemDaySeparatorFactory @Inject constructor(
|
||||
private val dateFormatter: DateFormatter,
|
||||
) {
|
||||
fun create(virtualItem: VirtualTimelineItem.DayDivider): TimelineItemVirtualModel {
|
||||
val formattedDate = daySeparatorFormatter.format(virtualItem.timestamp)
|
||||
val formattedDate = dateFormatter.format(
|
||||
timestamp = virtualItem.timestamp,
|
||||
mode = DateFormatterMode.Day,
|
||||
useRelative = true,
|
||||
)
|
||||
return TimelineItemDaySeparatorModel(
|
||||
formattedDate = formattedDate
|
||||
)
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ sealed interface TimelineItem {
|
|||
val senderProfile: ProfileTimelineDetails,
|
||||
val senderAvatar: AvatarData,
|
||||
val content: TimelineItemEventContent,
|
||||
val sentTimeMillis: Long = 0L,
|
||||
val sentTime: String = "",
|
||||
val isMine: Boolean = false,
|
||||
val isEditable: Boolean,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,13 @@ open class TimelineItemEncryptedContentProvider : PreviewParameterProvider<Timel
|
|||
aTimelineItemEncryptedContent(
|
||||
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||
sessionId = "sessionId",
|
||||
utdCause = UtdCause.HistoricalMessage,
|
||||
utdCause = UtdCause.HistoricalMessageAndBackupIsDisabled,
|
||||
)
|
||||
),
|
||||
aTimelineItemEncryptedContent(
|
||||
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||
sessionId = "sessionId",
|
||||
utdCause = UtdCause.HistoricalMessageAndDeviceIsUnverified,
|
||||
)
|
||||
),
|
||||
aTimelineItemEncryptedContent(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import androidx.core.net.toUri
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import io.element.android.features.messages.api.MessageComposerContext
|
||||
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||
|
|
@ -29,6 +28,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter
|
|||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageException
|
||||
import io.element.android.libraries.voicerecorder.api.VoiceRecorder
|
||||
import io.element.android.libraries.voicerecorder.api.VoiceRecorderState
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
|
|
|||
|
|
@ -8,11 +8,6 @@
|
|||
package io.element.android.features.messages.impl.voicemessages.timeline
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -23,17 +18,10 @@ import dagger.multibindings.IntoMap
|
|||
import io.element.android.features.messages.impl.timeline.di.TimelineItemEventContentKey
|
||||
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactory
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.ui.utils.time.formatShort
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||
|
||||
@Module
|
||||
@ContributesTo(RoomScope::class)
|
||||
|
|
@ -45,9 +33,7 @@ interface VoiceMessagePresenterModule {
|
|||
}
|
||||
|
||||
class VoiceMessagePresenter @AssistedInject constructor(
|
||||
voiceMessagePlayerFactory: VoiceMessagePlayer.Factory,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val scope: CoroutineScope,
|
||||
voiceMessagePresenterFactory: VoiceMessagePresenterFactory,
|
||||
@Assisted private val content: TimelineItemVoiceContent,
|
||||
) : Presenter<VoiceMessageState> {
|
||||
@AssistedFactory
|
||||
|
|
@ -55,97 +41,16 @@ class VoiceMessagePresenter @AssistedInject constructor(
|
|||
override fun create(content: TimelineItemVoiceContent): VoiceMessagePresenter
|
||||
}
|
||||
|
||||
private val player = voiceMessagePlayerFactory.create(
|
||||
private val presenter = voiceMessagePresenterFactory.createVoiceMessagePresenter(
|
||||
eventId = content.eventId,
|
||||
mediaSource = content.mediaSource,
|
||||
mimeType = content.mimeType,
|
||||
filename = content.filename,
|
||||
duration = content.duration,
|
||||
)
|
||||
|
||||
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
||||
|
||||
@Composable
|
||||
override fun present(): VoiceMessageState {
|
||||
val playerState by player.state.collectAsState(
|
||||
VoiceMessagePlayer.State(
|
||||
isReady = false,
|
||||
isPlaying = false,
|
||||
isEnded = false,
|
||||
currentPosition = 0L,
|
||||
duration = null
|
||||
)
|
||||
)
|
||||
|
||||
val button by remember {
|
||||
derivedStateOf {
|
||||
when {
|
||||
content.eventId == null -> VoiceMessageState.Button.Disabled
|
||||
playerState.isPlaying -> VoiceMessageState.Button.Pause
|
||||
play.value is AsyncData.Loading -> VoiceMessageState.Button.Downloading
|
||||
play.value is AsyncData.Failure -> VoiceMessageState.Button.Retry
|
||||
else -> VoiceMessageState.Button.Play
|
||||
}
|
||||
}
|
||||
}
|
||||
val duration by remember {
|
||||
derivedStateOf { playerState.duration ?: content.duration.inWholeMilliseconds }
|
||||
}
|
||||
val progress by remember {
|
||||
derivedStateOf {
|
||||
playerState.currentPosition / duration.toFloat()
|
||||
}
|
||||
}
|
||||
val time by remember {
|
||||
derivedStateOf {
|
||||
when {
|
||||
playerState.isReady && !playerState.isEnded -> playerState.currentPosition
|
||||
playerState.currentPosition > 0 -> playerState.currentPosition
|
||||
else -> duration
|
||||
}.milliseconds.formatShort()
|
||||
}
|
||||
}
|
||||
val showCursor by remember {
|
||||
derivedStateOf {
|
||||
!play.value.isUninitialized() && !playerState.isEnded
|
||||
}
|
||||
}
|
||||
|
||||
fun eventSink(event: VoiceMessageEvents) {
|
||||
when (event) {
|
||||
is VoiceMessageEvents.PlayPause -> {
|
||||
if (playerState.isPlaying) {
|
||||
player.pause()
|
||||
} else if (playerState.isReady) {
|
||||
player.play()
|
||||
} else {
|
||||
scope.launch {
|
||||
play.runUpdatingState(
|
||||
errorTransform = {
|
||||
analyticsService.trackError(
|
||||
VoiceMessageException.PlayMessageError("Error while trying to play voice message", it)
|
||||
)
|
||||
it
|
||||
},
|
||||
) {
|
||||
player.prepare().flatMap {
|
||||
runCatching { player.play() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is VoiceMessageEvents.Seek -> {
|
||||
player.seekTo((event.percentage * duration).toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return VoiceMessageState(
|
||||
button = button,
|
||||
progress = progress,
|
||||
time = time,
|
||||
showCursor = showCursor,
|
||||
eventSink = { eventSink(it) },
|
||||
)
|
||||
return presenter.present()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ class MessagesPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent())))
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EditPoll, aMessageEvent(content = aTimelineItemPollContent())))
|
||||
awaitItem()
|
||||
onEditPollClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ class MessagesViewTest {
|
|||
actionListState = anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = timelineItem,
|
||||
sentTimeFull = "",
|
||||
displayEmojiReactions = true,
|
||||
actions = persistentListOf(TimelineItemAction.Edit),
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
|
|
@ -399,6 +400,7 @@ class MessagesViewTest {
|
|||
actionListState = anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = timelineItem,
|
||||
sentTimeFull = "",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(TimelineItemAction.Edit),
|
||||
|
|
@ -427,6 +429,7 @@ class MessagesViewTest {
|
|||
actionListState = anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = timelineItem,
|
||||
sentTimeFull = "",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = aChangedIdentitySendFailure(),
|
||||
actions = persistentListOf(),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
|
|||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
||||
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
|
|
@ -86,6 +87,7 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = false,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
|
|
@ -128,6 +130,7 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = false,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
|
|
@ -170,13 +173,14 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.ReportContent,
|
||||
|
|
@ -215,13 +219,14 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.ReplyInThread,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.ReportContent,
|
||||
|
|
@ -263,12 +268,13 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.ReportContent,
|
||||
|
|
@ -308,13 +314,14 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.ReportContent,
|
||||
|
|
@ -355,13 +362,14 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = false,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.ReportContent,
|
||||
|
|
@ -403,14 +411,15 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.Redact,
|
||||
|
|
@ -448,14 +457,15 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.ReplyInThread,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.Redact,
|
||||
|
|
@ -496,14 +506,15 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.ViewSource,
|
||||
)
|
||||
|
|
@ -542,14 +553,15 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.AddCaption,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
|
|
@ -592,6 +604,7 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
|
|
@ -599,8 +612,8 @@ class ActionListPresenterTest {
|
|||
TimelineItemAction.Forward,
|
||||
// Not here
|
||||
// TimelineItemAction.AddCaption,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
|
|
@ -641,14 +654,15 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.EditCaption,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyCaption,
|
||||
TimelineItemAction.RemoveCaption,
|
||||
TimelineItemAction.ViewSource,
|
||||
|
|
@ -691,13 +705,14 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyCaption,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.ReportContent,
|
||||
|
|
@ -738,6 +753,7 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = stateEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = false,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
|
|
@ -808,14 +824,15 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
|
|
@ -855,13 +872,14 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.Redact,
|
||||
|
|
@ -909,14 +927,15 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Unpin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Unpin,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.Redact,
|
||||
|
|
@ -1006,6 +1025,7 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
|
|
@ -1046,14 +1066,15 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.EndPoll,
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.EditPoll,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
)
|
||||
|
|
@ -1089,13 +1110,14 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.EndPoll,
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
)
|
||||
|
|
@ -1131,12 +1153,13 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
)
|
||||
|
|
@ -1174,13 +1197,14 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
)
|
||||
|
|
@ -1214,6 +1238,7 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
sentTimeFull = "0 Full true",
|
||||
displayEmojiReactions = false,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = persistentListOf(
|
||||
|
|
@ -1268,6 +1293,7 @@ private fun createActionListPresenter(
|
|||
initialState = mapOf(
|
||||
FeatureFlags.MediaCaptionCreation.key to allowCaption,
|
||||
),
|
||||
)
|
||||
),
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.actionlist.model
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
class TimelineItemActionComparatorTest {
|
||||
@Test
|
||||
fun `check that the list in the comparator only contain each item once`() {
|
||||
val sut = TimelineItemActionComparator()
|
||||
sut.orderedList.forEach {
|
||||
require(sut.orderedList.count { item -> item == it } == 1, { "Duplicate ${it::class.java}.$it" })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check that the list in the comparator contains all the items`() {
|
||||
val sut = TimelineItemActionComparator()
|
||||
TimelineItemAction.entries.forEach {
|
||||
require(it in sut.orderedList, { "Missing ${it::class.simpleName}.$it in orderedList" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,8 +28,7 @@ import io.element.android.features.messages.impl.utils.FakeTextPillificationHelp
|
|||
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
||||
import io.element.android.features.poll.test.pollcontent.FakePollContentStateFactory
|
||||
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
|
|
@ -80,7 +79,7 @@ internal fun TestScope.aTimelineItemsFactory(
|
|||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(),
|
||||
),
|
||||
matrixClient = matrixClient,
|
||||
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
config = config
|
||||
)
|
||||
|
|
@ -88,7 +87,7 @@ internal fun TestScope.aTimelineItemsFactory(
|
|||
},
|
||||
virtualItemFactory = TimelineItemVirtualFactory(
|
||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||
FakeDaySeparatorFormatter()
|
||||
FakeDateFormatter()
|
||||
),
|
||||
),
|
||||
timelineItemGrouper = TimelineItemGrouper(),
|
||||
|
|
|
|||
|
|
@ -27,24 +27,7 @@ class PinnedMessagesListTimelineActionPostProcessorTest {
|
|||
fun `ensure that some actions are kept and some other are filtered out`() {
|
||||
val sut = PinnedMessagesListTimelineActionPostProcessor()
|
||||
val result = sut.process(
|
||||
listOf(
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.CopyText,
|
||||
TimelineItemAction.CopyCaption,
|
||||
TimelineItemAction.CopyLink,
|
||||
TimelineItemAction.Redact,
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.ReplyInThread,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.EditCaption,
|
||||
TimelineItemAction.AddCaption,
|
||||
TimelineItemAction.RemoveCaption,
|
||||
TimelineItemAction.ViewSource,
|
||||
TimelineItemAction.ReportContent,
|
||||
TimelineItemAction.EndPoll,
|
||||
TimelineItemAction.Pin,
|
||||
TimelineItemAction.Unpin,
|
||||
)
|
||||
TimelineItemAction.entries.toList()
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import io.element.android.features.messages.impl.messagecomposer.aReplyMode
|
||||
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
|
||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
|
|
@ -36,6 +35,7 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
|||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageException
|
||||
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ package io.element.android.features.poll.impl.history.model
|
|||
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
|
@ -18,7 +19,7 @@ import javax.inject.Inject
|
|||
|
||||
class PollHistoryItemsFactory @Inject constructor(
|
||||
private val pollContentStateFactory: PollContentStateFactory,
|
||||
private val daySeparatorFormatter: DaySeparatorFormatter,
|
||||
private val dateFormatter: DateFormatter,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
suspend fun create(timelineItems: List<MatrixTimelineItem>): PollHistoryItems = withContext(dispatchers.computation) {
|
||||
|
|
@ -45,7 +46,11 @@ class PollHistoryItemsFactory @Inject constructor(
|
|||
val pollContent = timelineItem.event.content as? PollContent ?: return null
|
||||
val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent)
|
||||
PollHistoryItem(
|
||||
formattedDate = daySeparatorFormatter.format(timelineItem.event.timestamp),
|
||||
formattedDate = dateFormatter.format(
|
||||
timestamp = timelineItem.event.timestamp,
|
||||
mode = DateFormatterMode.Day,
|
||||
useRelative = true
|
||||
),
|
||||
state = pollContentState
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import io.element.android.features.poll.impl.history.model.PollHistoryItemsFacto
|
|||
import io.element.android.features.poll.impl.model.DefaultPollContentStateFactory
|
||||
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
|
|
@ -161,7 +161,7 @@ class PollHistoryPresenterTest {
|
|||
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
||||
pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory(
|
||||
pollContentStateFactory = DefaultPollContentStateFactory(FakeMatrixClient()),
|
||||
daySeparatorFormatter = FakeDaySeparatorFormatter(),
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
),
|
||||
): PollHistoryPresenter {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.element.android.features.logout.api.LogoutUseCase
|
|||
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
||||
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
|
|
@ -63,7 +64,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
mutableStateOf<AsyncData<String>>(AsyncData.Uninitialized)
|
||||
}
|
||||
val clearCacheAction = remember {
|
||||
mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
||||
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||
}
|
||||
val customElementCallBaseUrl by appPreferencesStore
|
||||
.getCustomElementCallBaseUrlFlow()
|
||||
|
|
@ -94,7 +95,7 @@ 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) {
|
||||
LaunchedEffect(clearCacheAction.value.isSuccess()) {
|
||||
computeCacheSize(cacheSize)
|
||||
}
|
||||
|
||||
|
|
@ -180,7 +181,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
}.runCatchingUpdatingState(cacheSize)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<AsyncData<Unit>>) = launch {
|
||||
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||
suspend {
|
||||
clearCacheUseCase()
|
||||
}.runCatchingUpdatingState(clearCacheAction)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.features.preferences.impl.developer
|
||||
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -16,7 +17,7 @@ data class DeveloperSettingsState(
|
|||
val features: ImmutableList<FeatureUiModel>,
|
||||
val cacheSize: AsyncData<String>,
|
||||
val rageshakeState: RageshakePreferencesState,
|
||||
val clearCacheAction: AsyncData<Unit>,
|
||||
val clearCacheAction: AsyncAction<Unit>,
|
||||
val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
|
||||
val isSimpleSlidingSyncEnabled: Boolean,
|
||||
val hideImagesAndVideos: Boolean,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.preferences.impl.developer
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList
|
||||
|
||||
|
|
@ -17,7 +18,7 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
|
|||
get() = sequenceOf(
|
||||
aDeveloperSettingsState(),
|
||||
aDeveloperSettingsState(
|
||||
clearCacheAction = AsyncData.Loading()
|
||||
clearCacheAction = AsyncAction.Loading
|
||||
),
|
||||
aDeveloperSettingsState(
|
||||
customElementCallBaseUrlState = aCustomElementCallBaseUrlState(
|
||||
|
|
@ -28,7 +29,7 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
|
|||
}
|
||||
|
||||
fun aDeveloperSettingsState(
|
||||
clearCacheAction: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||
clearCacheAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(),
|
||||
isSimplifiedSlidingSyncEnabled: Boolean = false,
|
||||
hideImagesAndVideos: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import io.element.android.features.preferences.impl.user.UserPreferences
|
|||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
|
||||
|
|
@ -33,7 +34,6 @@ import io.element.android.libraries.designsystem.theme.components.IconSource
|
|||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
|
|
@ -270,7 +270,7 @@ private fun ColumnScope.Footer(
|
|||
private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_developer_options)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_developer_options)),
|
||||
leadingContent = ListItemContent.Icon(IconSource.Resource(CompoundDrawables.ic_compound_code)),
|
||||
onClick = onOpenDeveloperSettings
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@
|
|||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.preferences.impl.developer
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.appconfig.ElementCallConfig
|
||||
import io.element.android.features.logout.test.FakeLogoutUseCase
|
||||
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
|
||||
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
|
||||
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
|
|
@ -24,8 +24,8 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
|||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
|
|
@ -38,37 +38,29 @@ class DeveloperSettingsPresenterTest {
|
|||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - ensures initial state is correct`() = runTest {
|
||||
fun `present - ensures initial states are correct`() = runTest {
|
||||
val presenter = createDeveloperSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.features).isEmpty()
|
||||
assertThat(initialState.clearCacheAction).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(initialState.cacheSize).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(initialState.customElementCallBaseUrlState).isNotNull()
|
||||
assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
|
||||
assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse()
|
||||
assertThat(initialState.hideImagesAndVideos).isFalse()
|
||||
val loadedState = awaitItem()
|
||||
assertThat(loadedState.rageshakeState.isEnabled).isFalse()
|
||||
assertThat(loadedState.rageshakeState.isSupported).isTrue()
|
||||
assertThat(loadedState.rageshakeState.sensitivity).isEqualTo(0.3f)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ensures feature list is loaded`() = runTest {
|
||||
val presenter = createDeveloperSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val state = awaitLastSequentialItem()
|
||||
val numberOfModifiableFeatureFlags = FeatureFlags.entries.count { it.isFinished.not() }
|
||||
assertThat(state.features).hasSize(numberOfModifiableFeatureFlags)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
presenter.test {
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.features).isEmpty()
|
||||
assertThat(state.clearCacheAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(state.cacheSize).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(state.customElementCallBaseUrlState).isNotNull()
|
||||
assertThat(state.customElementCallBaseUrlState.baseUrl).isNull()
|
||||
assertThat(state.isSimpleSlidingSyncEnabled).isFalse()
|
||||
assertThat(state.hideImagesAndVideos).isFalse()
|
||||
assertThat(state.rageshakeState.isEnabled).isFalse()
|
||||
assertThat(state.rageshakeState.isSupported).isTrue()
|
||||
assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.features).isNotEmpty()
|
||||
val numberOfModifiableFeatureFlags = FeatureFlags.entries.count { it.isFinished.not() }
|
||||
assertThat(state.features).hasSize(numberOfModifiableFeatureFlags)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.cacheSize).isInstanceOf(AsyncData.Success::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,30 +68,28 @@ class DeveloperSettingsPresenterTest {
|
|||
fun `present - ensures Room directory search is not present on release Google Play builds`() = runTest {
|
||||
val buildMeta = aBuildMeta(buildType = BuildType.RELEASE, flavorDescription = "GooglePlay")
|
||||
val presenter = createDeveloperSettingsPresenter(buildMeta = buildMeta)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val state = awaitLastSequentialItem()
|
||||
assertThat(state.features).doesNotContain(FeatureFlags.RoomDirectorySearch)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.features).doesNotContain(FeatureFlags.RoomDirectorySearch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
|
||||
val presenter = createDeveloperSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val stateBeforeEvent = awaitItem()
|
||||
val featureBeforeEvent = stateBeforeEvent.features.first()
|
||||
stateBeforeEvent.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(featureBeforeEvent, !featureBeforeEvent.isEnabled))
|
||||
val stateAfterEvent = awaitItem()
|
||||
val featureAfterEvent = stateAfterEvent.features.first()
|
||||
assertThat(featureBeforeEvent.key).isEqualTo(featureAfterEvent.key)
|
||||
assertThat(featureBeforeEvent.isEnabled).isNotEqualTo(featureAfterEvent.isEnabled)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
awaitItem().also { state ->
|
||||
val feature = state.features.first()
|
||||
state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, !feature.isEnabled))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
val feature = state.features.first()
|
||||
assertThat(feature.isEnabled).isTrue()
|
||||
assertThat(feature.key).isEqualTo(feature.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,19 +97,25 @@ class DeveloperSettingsPresenterTest {
|
|||
fun `present - clear cache`() = runTest {
|
||||
val clearCacheUseCase = FakeClearCacheUseCase()
|
||||
val presenter = createDeveloperSettingsPresenter(clearCacheUseCase = clearCacheUseCase)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
assertThat(clearCacheUseCase.executeHasBeenCalled).isFalse()
|
||||
initialState.eventSink(DeveloperSettingsEvents.ClearCache)
|
||||
val stateAfterEvent = awaitItem()
|
||||
assertThat(stateAfterEvent.clearCacheAction).isInstanceOf(AsyncData.Loading::class.java)
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().clearCacheAction).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
awaitItem().also { state ->
|
||||
state.eventSink(DeveloperSettingsEvents.ClearCache)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.clearCacheAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.clearCacheAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.cacheSize).isInstanceOf(AsyncData.Loading::class.java)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.cacheSize).isInstanceOf(AsyncData.Success::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,26 +123,25 @@ class DeveloperSettingsPresenterTest {
|
|||
fun `present - custom element call base url`() = runTest {
|
||||
val preferencesStore = InMemoryAppPreferencesStore()
|
||||
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
|
||||
initialState.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.ahoy"))
|
||||
val updatedItem = awaitItem()
|
||||
assertThat(updatedItem.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy")
|
||||
assertThat(updatedItem.customElementCallBaseUrlState.defaultUrl).isEqualTo(ElementCallConfig.DEFAULT_BASE_URL)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.customElementCallBaseUrlState.baseUrl).isNull()
|
||||
state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.ahoy"))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy")
|
||||
assertThat(state.customElementCallBaseUrlState.defaultUrl).isEqualTo(ElementCallConfig.DEFAULT_BASE_URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - custom element call base url validator needs at least an HTTP scheme and host`() = runTest {
|
||||
val presenter = createDeveloperSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val urlValidator = awaitLastSequentialItem().customElementCallBaseUrlState.validator
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
val urlValidator = awaitItem().customElementCallBaseUrlState.validator
|
||||
assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one
|
||||
assertThat(urlValidator("test")).isFalse()
|
||||
assertThat(urlValidator("http://")).isFalse()
|
||||
|
|
@ -155,30 +150,31 @@ class DeveloperSettingsPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - toggling simplified sliding sync changes the preferences and logs out the user`() = runTest {
|
||||
val logoutCallRecorder = lambdaRecorder<Boolean, String?> { "" }
|
||||
val logoutUseCase = FakeLogoutUseCase(logoutLambda = logoutCallRecorder)
|
||||
val preferences = InMemoryAppPreferencesStore()
|
||||
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences, logoutUseCase = logoutUseCase)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse()
|
||||
|
||||
initialState.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true))
|
||||
assertThat(awaitItem().isSimpleSlidingSyncEnabled).isTrue()
|
||||
assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
|
||||
advanceUntilIdle()
|
||||
logoutCallRecorder.assertions().isCalledOnce()
|
||||
|
||||
initialState.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(false))
|
||||
assertThat(awaitItem().isSimpleSlidingSyncEnabled).isFalse()
|
||||
assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
|
||||
advanceUntilIdle()
|
||||
logoutCallRecorder.assertions().isCalledExactly(times = 2)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isSimpleSlidingSyncEnabled).isFalse()
|
||||
state.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isSimpleSlidingSyncEnabled).isTrue()
|
||||
assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
|
||||
advanceUntilIdle()
|
||||
logoutCallRecorder.assertions().isCalledOnce()
|
||||
state.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(false))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isSimpleSlidingSyncEnabled).isFalse()
|
||||
assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
|
||||
advanceUntilIdle()
|
||||
logoutCallRecorder.assertions().isCalledExactly(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,17 +182,21 @@ class DeveloperSettingsPresenterTest {
|
|||
fun `present - toggling hide image and video`() = runTest {
|
||||
val preferences = InMemoryAppPreferencesStore()
|
||||
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
assertThat(initialState.hideImagesAndVideos).isFalse()
|
||||
initialState.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(true))
|
||||
assertThat(awaitItem().hideImagesAndVideos).isTrue()
|
||||
assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue()
|
||||
initialState.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(false))
|
||||
assertThat(awaitItem().hideImagesAndVideos).isFalse()
|
||||
assertThat(preferences.doesHideImagesAndVideosFlow().first()).isFalse()
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.hideImagesAndVideos).isFalse()
|
||||
state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(true))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.hideImagesAndVideos).isTrue()
|
||||
assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue()
|
||||
state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(false))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.hideImagesAndVideos).isFalse()
|
||||
assertThat(preferences.doesHideImagesAndVideosFlow().first()).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
<string name="screen_room_details_requests_to_join_title">"Žádosti o vstup"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Role a oprávnění"</string>
|
||||
<string name="screen_room_details_room_name_label">"Název místnosti"</string>
|
||||
<string name="screen_room_details_security_and_privacy_title">"Zabezpečení a soukromí"</string>
|
||||
<string name="screen_room_details_security_title">"Zabezpečení"</string>
|
||||
<string name="screen_room_details_share_room_title">"Sdílet místnost"</string>
|
||||
<string name="screen_room_details_title">"Informace o místnosti"</string>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
<string name="screen_room_details_invite_people_title">"Πρόσκληση ατόμων"</string>
|
||||
<string name="screen_room_details_leave_conversation_title">"Αποχώρηση από τη συζήτηση"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Αποχώρηση από το δωμάτιο"</string>
|
||||
<string name="screen_room_details_media_gallery_title">"Πολυμέσα και αρχεία"</string>
|
||||
<string name="screen_room_details_notification_mode_custom">"Προσαρμοσμένο"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"Προεπιλογή"</string>
|
||||
<string name="screen_room_details_notification_title">"Ειδοποιήσεις"</string>
|
||||
|
|
@ -56,6 +57,7 @@
|
|||
<string name="screen_room_details_requests_to_join_title">"Αιτήματα συμμετοχής"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Ρόλοι και δικαιώματα"</string>
|
||||
<string name="screen_room_details_room_name_label">"Όνομα δωματίου"</string>
|
||||
<string name="screen_room_details_security_and_privacy_title">"Ασφάλεια & απόρρητο"</string>
|
||||
<string name="screen_room_details_security_title">"Ασφάλεια"</string>
|
||||
<string name="screen_room_details_share_room_title">"Κοινή χρήση δωματίου"</string>
|
||||
<string name="screen_room_details_title">"Πληροφορίες δωματίου"</string>
|
||||
|
|
|
|||
|
|
@ -49,10 +49,12 @@
|
|||
<string name="screen_room_details_invite_people_title">"Kutsu ihmisiä"</string>
|
||||
<string name="screen_room_details_leave_conversation_title">"Poistu keskustelusta"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Poistu huoneesta"</string>
|
||||
<string name="screen_room_details_media_gallery_title">"Media ja tiedostot"</string>
|
||||
<string name="screen_room_details_notification_mode_custom">"Mukautettu"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"Oletus"</string>
|
||||
<string name="screen_room_details_notification_title">"Ilmoitukset"</string>
|
||||
<string name="screen_room_details_pinned_events_row_title">"Kiinnitetyt viestit"</string>
|
||||
<string name="screen_room_details_requests_to_join_title">"Liittymispyynnöt"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Roolit ja oikeudet"</string>
|
||||
<string name="screen_room_details_room_name_label">"Huoneen nimi"</string>
|
||||
<string name="screen_room_details_security_title">"Turvallisuus"</string>
|
||||
|
|
@ -80,7 +82,7 @@
|
|||
<string name="screen_room_member_list_manage_member_user_info">"Näytä profiili"</string>
|
||||
<string name="screen_room_member_list_mode_banned">"Porttikiellot"</string>
|
||||
<string name="screen_room_member_list_mode_members">"Jäsenet"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"Kutsutut"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"Kutsuttu"</string>
|
||||
<string name="screen_room_member_list_removing_user">"Poistetaan käyttäjää %1$s huoneesta…"</string>
|
||||
<string name="screen_room_member_list_role_administrator">"Ylläpitäjä"</string>
|
||||
<string name="screen_room_member_list_role_moderator">"Valvoja"</string>
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
<string name="screen_room_details_requests_to_join_title">"Demandes en attente"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Rôles et autorisations"</string>
|
||||
<string name="screen_room_details_room_name_label">"Nom du salon"</string>
|
||||
<string name="screen_room_details_security_and_privacy_title">"Sécurité & confidentialité"</string>
|
||||
<string name="screen_room_details_security_title">"Sécurité"</string>
|
||||
<string name="screen_room_details_share_room_title">"Partager le salon"</string>
|
||||
<string name="screen_room_details_title">"Informations du salon"</string>
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
<string name="screen_room_details_requests_to_join_title">"Csatlakozási kérelem"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Szerepkörök és jogosultságok"</string>
|
||||
<string name="screen_room_details_room_name_label">"Szoba neve"</string>
|
||||
<string name="screen_room_details_security_and_privacy_title">"Biztonság és adatvédelem"</string>
|
||||
<string name="screen_room_details_security_title">"Biztonság"</string>
|
||||
<string name="screen_room_details_share_room_title">"Szoba megosztása"</string>
|
||||
<string name="screen_room_details_title">"Szobainformációk"</string>
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
<string name="screen_room_details_requests_to_join_title">"Requests to join"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Roles and permissions"</string>
|
||||
<string name="screen_room_details_room_name_label">"Room name"</string>
|
||||
<string name="screen_room_details_security_and_privacy_title">"Security & privacy"</string>
|
||||
<string name="screen_room_details_security_title">"Security"</string>
|
||||
<string name="screen_room_details_share_room_title">"Share room"</string>
|
||||
<string name="screen_room_details_title">"Room info"</string>
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ fun RoomListContextMenu(
|
|||
onFavoriteChange = { isFavorite ->
|
||||
eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
|
||||
},
|
||||
onClearCacheRoomClick = {
|
||||
eventSink(RoomListEvents.HideContextMenu)
|
||||
eventSink(RoomListEvents.ClearCacheOfRoom(contextMenu.roomId))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -73,6 +77,7 @@ private fun RoomListModalBottomSheetContent(
|
|||
onFavoriteChange: (isFavorite: Boolean) -> Unit,
|
||||
onRoomMarkReadClick: () -> Unit,
|
||||
onRoomMarkUnreadClick: () -> Unit,
|
||||
onClearCacheRoomClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
|
|
@ -177,6 +182,18 @@ private fun RoomListModalBottomSheetContent(
|
|||
),
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
if (contextMenu.eventCacheFeatureFlagEnabled) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = "Clear cache for this room")
|
||||
},
|
||||
modifier = Modifier.clickable { onClearCacheRoomClick() },
|
||||
leadingContent = ListItemContent.Icon(
|
||||
iconSource = IconSource.Vector(CompoundIcons.Delete())
|
||||
),
|
||||
style = ListItemStyle.Primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -195,5 +212,6 @@ internal fun RoomListModalBottomSheetContentPreview(
|
|||
onRoomSettingsClick = {},
|
||||
onLeaveRoomClick = {},
|
||||
onFavoriteChange = {},
|
||||
onClearCacheRoomClick = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@ sealed interface RoomListEvents {
|
|||
data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents
|
||||
data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents
|
||||
data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents
|
||||
data class ClearCacheOfRoom(val roomId: RoomId) : ContextMenuEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ class RoomListPresenter @Inject constructor(
|
|||
AcceptDeclineInviteEvents.DeclineInvite(event.roomListRoomSummary.toInviteData())
|
||||
)
|
||||
}
|
||||
is RoomListEvents.ClearCacheOfRoom -> coroutineScope.clearCacheOfRoom(event.roomId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +256,8 @@ class RoomListPresenter @Inject constructor(
|
|||
isDm = event.roomListRoomSummary.isDm,
|
||||
isFavorite = event.roomListRoomSummary.isFavorite,
|
||||
markAsUnreadFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.MarkAsUnread),
|
||||
hasNewContent = event.roomListRoomSummary.hasNewContent
|
||||
hasNewContent = event.roomListRoomSummary.hasNewContent,
|
||||
eventCacheFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.EventCache),
|
||||
)
|
||||
contextMenuState.value = initialState
|
||||
|
||||
|
|
@ -312,6 +314,12 @@ class RoomListPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.clearCacheOfRoom(roomId: RoomId) = launch {
|
||||
client.getRoom(roomId)?.use { room ->
|
||||
room.clearEventCacheStorage()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user needs to migrate to a native sliding sync version.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ data class RoomListState(
|
|||
val isDm: Boolean,
|
||||
val isFavorite: Boolean,
|
||||
val markAsUnreadFeatureFlagEnabled: Boolean,
|
||||
val eventCacheFeatureFlagEnabled: Boolean,
|
||||
val hasNewContent: Boolean,
|
||||
) : ContextMenu
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,4 +31,5 @@ internal fun aContextMenuShown(
|
|||
markAsUnreadFeatureFlagEnabled = true,
|
||||
hasNewContent = hasNewContent,
|
||||
isFavorite = isFavorite,
|
||||
eventCacheFeatureFlagEnabled = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ package io.element.android.features.roomlist.impl.datasource
|
|||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
|
|
@ -22,7 +23,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import javax.inject.Inject
|
||||
|
||||
class RoomListRoomSummaryFactory @Inject constructor(
|
||||
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
|
||||
private val dateFormatter: DateFormatter,
|
||||
private val roomLastMessageFormatter: RoomLastMessageFormatter,
|
||||
) {
|
||||
fun create(roomSummary: RoomSummary): RoomListRoomSummary {
|
||||
|
|
@ -36,7 +37,11 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
|||
numberOfUnreadMentions = roomInfo.numUnreadMentions,
|
||||
numberOfUnreadNotifications = roomInfo.numUnreadNotifications,
|
||||
isMarkedUnread = roomInfo.isMarkedUnread,
|
||||
timestamp = lastMessageTimestampFormatter.format(roomSummary.lastMessageTimestamp),
|
||||
timestamp = dateFormatter.format(
|
||||
timestamp = roomSummary.lastMessageTimestamp,
|
||||
mode = DateFormatterMode.TimeOrDate,
|
||||
useRelative = true,
|
||||
),
|
||||
lastMessage = roomSummary.lastMessage?.let { message ->
|
||||
roomLastMessageFormatter.format(message.event, roomInfo.isDm)
|
||||
}.orEmpty(),
|
||||
|
|
|
|||
|
|
@ -31,9 +31,8 @@ import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
|||
import io.element.android.features.roomlist.impl.search.aRoomListSearchState
|
||||
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
|
||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
||||
|
|
@ -188,6 +187,7 @@ class RoomListPresenterTest {
|
|||
createRoomListRoomSummary(
|
||||
numberOfUnreadMentions = 1,
|
||||
numberOfUnreadMessages = 2,
|
||||
timestamp = "0 TimeOrDate true",
|
||||
)
|
||||
)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
|
|
@ -288,6 +288,7 @@ class RoomListPresenterTest {
|
|||
isDm = false,
|
||||
isFavorite = false,
|
||||
markAsUnreadFeatureFlagEnabled = true,
|
||||
eventCacheFeatureFlagEnabled = false,
|
||||
hasNewContent = false,
|
||||
)
|
||||
)
|
||||
|
|
@ -305,6 +306,7 @@ class RoomListPresenterTest {
|
|||
isDm = false,
|
||||
isFavorite = true,
|
||||
markAsUnreadFeatureFlagEnabled = true,
|
||||
eventCacheFeatureFlagEnabled = false,
|
||||
hasNewContent = false,
|
||||
)
|
||||
)
|
||||
|
|
@ -335,6 +337,7 @@ class RoomListPresenterTest {
|
|||
isDm = false,
|
||||
isFavorite = false,
|
||||
markAsUnreadFeatureFlagEnabled = true,
|
||||
eventCacheFeatureFlagEnabled = false,
|
||||
hasNewContent = false,
|
||||
)
|
||||
)
|
||||
|
|
@ -633,9 +636,7 @@ class RoomListPresenterTest {
|
|||
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
||||
lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply {
|
||||
givenFormat(A_FORMATTED_DATE)
|
||||
},
|
||||
dateFormatter: DateFormatter = FakeDateFormatter(),
|
||||
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
|
|
@ -652,7 +653,7 @@ class RoomListPresenterTest {
|
|||
roomListDataSource = RoomListDataSource(
|
||||
roomListService = client.roomListService,
|
||||
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||
lastMessageTimestampFormatter = lastMessageTimestampFormatter,
|
||||
dateFormatter = dateFormatter,
|
||||
roomLastMessageFormatter = roomLastMessageFormatter,
|
||||
),
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.roomlist.impl.FakeDateTimeObserver
|
||||
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||
|
|
@ -30,12 +30,12 @@ class RoomListDataSourceTest {
|
|||
postAllRooms(listOf(aRoomSummary()))
|
||||
}
|
||||
val dateTimeObserver = FakeDateTimeObserver()
|
||||
val lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter()
|
||||
lastMessageTimestampFormatter.givenFormat("Today")
|
||||
var dateFormatterResult = "Today"
|
||||
val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult })
|
||||
val roomListDataSource = createRoomListDataSource(
|
||||
roomListService = roomListService,
|
||||
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||
lastMessageTimestampFormatter = lastMessageTimestampFormatter,
|
||||
dateFormatter = dateFormatter,
|
||||
),
|
||||
dateTimeObserver = dateTimeObserver,
|
||||
)
|
||||
|
|
@ -47,7 +47,7 @@ class RoomListDataSourceTest {
|
|||
val initialRoomList = awaitItem()
|
||||
assertThat(initialRoomList).isNotEmpty()
|
||||
assertThat(initialRoomList.first().timestamp).isEqualTo("Today")
|
||||
lastMessageTimestampFormatter.givenFormat("Yesterday")
|
||||
dateFormatterResult = "Yesterday"
|
||||
// Trigger a date change
|
||||
dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now()))
|
||||
// Check there is a new list and it's not the same as the previous one
|
||||
|
|
@ -64,12 +64,12 @@ class RoomListDataSourceTest {
|
|||
postAllRooms(listOf(aRoomSummary()))
|
||||
}
|
||||
val dateTimeObserver = FakeDateTimeObserver()
|
||||
val lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter()
|
||||
lastMessageTimestampFormatter.givenFormat("Today")
|
||||
var dateFormatterResult = "Today"
|
||||
val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult })
|
||||
val roomListDataSource = createRoomListDataSource(
|
||||
roomListService = roomListService,
|
||||
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||
lastMessageTimestampFormatter = lastMessageTimestampFormatter,
|
||||
dateFormatter = dateFormatter,
|
||||
),
|
||||
dateTimeObserver = dateTimeObserver,
|
||||
)
|
||||
|
|
@ -80,7 +80,7 @@ class RoomListDataSourceTest {
|
|||
val initialRoomList = awaitItem()
|
||||
assertThat(initialRoomList).isNotEmpty()
|
||||
assertThat(initialRoomList.first().timestamp).isEqualTo("Today")
|
||||
lastMessageTimestampFormatter.givenFormat("Yesterday")
|
||||
dateFormatterResult = "Yesterday"
|
||||
// Trigger a timezone change
|
||||
dateTimeObserver.given(DateTimeObserver.Event.TimeZoneChanged)
|
||||
// Check there is a new list and it's not the same as the previous one
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@
|
|||
|
||||
package io.element.android.features.roomlist.impl.datasource
|
||||
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
|
||||
fun aRoomListRoomSummaryFactory(
|
||||
lastMessageTimestampFormatter: LastMessageTimestampFormatter = LastMessageTimestampFormatter { _ -> "Today" },
|
||||
dateFormatter: DateFormatter = FakeDateFormatter { _, _, _ -> "Today" },
|
||||
roomLastMessageFormatter: RoomLastMessageFormatter = RoomLastMessageFormatter { _, _ -> "Hey" }
|
||||
) = RoomListRoomSummaryFactory(
|
||||
lastMessageTimestampFormatter = lastMessageTimestampFormatter,
|
||||
dateFormatter = dateFormatter,
|
||||
roomLastMessageFormatter = roomLastMessageFormatter
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
package io.element.android.features.roomlist.impl.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
|
|
@ -84,6 +83,7 @@ internal fun createRoomListRoomSummary(
|
|||
isFavorite: Boolean = false,
|
||||
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
||||
heroes: List<AvatarData> = emptyList(),
|
||||
timestamp: String? = null,
|
||||
) = RoomListRoomSummary(
|
||||
id = A_ROOM_ID.value,
|
||||
roomId = A_ROOM_ID,
|
||||
|
|
@ -92,7 +92,7 @@ internal fun createRoomListRoomSummary(
|
|||
numberOfUnreadMessages = numberOfUnreadMessages,
|
||||
numberOfUnreadNotifications = numberOfUnreadNotifications,
|
||||
isMarkedUnread = isMarkedUnread,
|
||||
timestamp = A_FORMATTED_DATE,
|
||||
timestamp = timestamp,
|
||||
lastMessage = "",
|
||||
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
|
||||
displayType = displayType,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.roomlist.impl.datasource.aRoomListRoomSummaryFactory
|
||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
|
|
@ -143,7 +143,7 @@ fun TestScope.createRoomListSearchPresenter(
|
|||
dataSource = RoomListSearchDataSource(
|
||||
roomListService = roomListService,
|
||||
roomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
roomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
||||
),
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<string name="screen_chat_backup_key_storage_toggle_title">"Salli avainten säilytys"</string>
|
||||
<string name="screen_chat_backup_recovery_action_change">"Vaihda palautusavain"</string>
|
||||
<string name="screen_chat_backup_recovery_action_change_description">"Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, jos olet menettänyt kaikki nykyiset laitteesi."</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm">"Käytä palautusavainta"</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm">"Syötä palautusavain"</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm_description">"Avainten säilytys ei ole tällä hetkellä synkronoitu."</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup">"Ota palautus käyttöön"</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup_description">"Pääset käsiksi salattuihin viesteihisi, jos menetät kaikki laitteesi tai olet kirjautunut ulos %1$s -sovelluksesta kaikkialla."</string>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<string name="screen_chat_backup_recovery_action_confirm">"Utiliser la clé de récupération"</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm_description">"Le stockage de vos clés est actuellement désynchronisé."</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup">"Configurer la sauvegarde"</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup_description">"Accédez à vos messages chiffrés si vous perdez tous vos appareils ou que vous êtes déconnectés de %1$s partout."</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup_description">"Accédez à vos messages chiffrés si vous perdez tous vos appareils ou que vous êtes déconnecté de %1$s partout."</string>
|
||||
<string name="screen_create_new_recovery_key_list_item_1">"Ouvrez %1$s sur un ordinateur"</string>
|
||||
<string name="screen_create_new_recovery_key_list_item_2">"Connectez-vous à nouveau à votre compte"</string>
|
||||
<string name="screen_create_new_recovery_key_list_item_3">"Lorsque vous devrez vérifier la session, choisissez %1$s"</string>
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
<string name="screen_key_backup_disable_confirmation_title">"Êtes-vous certain de vouloir désactiver la sauvegarde ?"</string>
|
||||
<string name="screen_key_backup_disable_description">"Désactiver la sauvegarde supprimera votre clé de récupération actuelle et désactivera d’autres mesures de sécurité. Dans ce cas :"</string>
|
||||
<string name="screen_key_backup_disable_description_point_1">"Pas d’accès à l’historique des discussions chiffrées sur vos nouveaux appareils"</string>
|
||||
<string name="screen_key_backup_disable_description_point_2">"Perte de l’accès à vos messages chiffrés si vous êtes déconnectés de %1$s partout"</string>
|
||||
<string name="screen_key_backup_disable_description_point_2">"Perte de l’accès à vos messages chiffrés si vous êtes déconnecté de %1$s partout"</string>
|
||||
<string name="screen_key_backup_disable_title">"Êtes-vous certain de vouloir désactiver la sauvegarde ?"</string>
|
||||
<string name="screen_recovery_key_change_description">"Obtenez une nouvelle clé de récupération dans le cas où vous avez oublié l’ancienne. Après le changement, l’ancienne clé ne sera plus utilisable."</string>
|
||||
<string name="screen_recovery_key_change_generate_key">"Générer une nouvelle clé"</string>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
|
|
@ -37,7 +38,7 @@ class IncomingVerificationPresenter @AssistedInject constructor(
|
|||
@Assisted private val navigator: IncomingVerificationNavigator,
|
||||
private val sessionVerificationService: SessionVerificationService,
|
||||
private val stateMachine: IncomingVerificationStateMachine,
|
||||
private val dateFormatter: LastMessageTimestampFormatter,
|
||||
private val dateFormatter: DateFormatter,
|
||||
) : Presenter<IncomingVerificationState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
@ -59,7 +60,10 @@ class IncomingVerificationPresenter @AssistedInject constructor(
|
|||
}
|
||||
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
|
||||
val formattedSignInTime = remember {
|
||||
dateFormatter.format(sessionVerificationRequestDetails.firstSeenTimestamp)
|
||||
dateFormatter.format(
|
||||
timestamp = sessionVerificationRequestDetails.firstSeenTimestamp,
|
||||
mode = DateFormatterMode.TimeOrDate,
|
||||
)
|
||||
}
|
||||
val step by remember {
|
||||
derivedStateOf {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<string name="screen_session_verification_compare_numbers_subtitle">"Varmista, että alla olevat numerot vastaavat toisessa istunnossa näkyviä numeroita."</string>
|
||||
<string name="screen_session_verification_compare_numbers_title">"Vertaa numeroita"</string>
|
||||
<string name="screen_session_verification_complete_subtitle">"Uusi kirjautumisesi on nyt vahvistettu. Sillä on pääsy salattuihin viesteihisi, ja muut käyttäjät näkevät sen luotettuna."</string>
|
||||
<string name="screen_session_verification_enter_recovery_key">"Käytä palautusavainta"</string>
|
||||
<string name="screen_session_verification_enter_recovery_key">"Syötä palautusavain"</string>
|
||||
<string name="screen_session_verification_failed_subtitle">"Joko pyyntö aikakatkaistiin, pyyntö hylättiin tai vahvistus ei täsmännyt."</string>
|
||||
<string name="screen_session_verification_open_existing_session_subtitle">"Vahvista, että se olet sinä, jotta näet aiemmat salatut viestisi."</string>
|
||||
<string name="screen_session_verification_open_existing_session_title">"Avaa laite, jossa olet jo kirjautuneena"</string>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@ package io.element.android.features.verifysession.impl.incoming
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
|
||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.matrix.api.core.FlowId
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
|
|
@ -56,7 +55,7 @@ class IncomingVerificationPresenterTest {
|
|||
IncomingVerificationState.Step.Initial(
|
||||
deviceDisplayName = "a device name",
|
||||
deviceId = A_DEVICE_ID,
|
||||
formattedSignInTime = A_FORMATTED_DATE,
|
||||
formattedSignInTime = "567 TimeOrDate false",
|
||||
isWaiting = false,
|
||||
)
|
||||
)
|
||||
|
|
@ -119,7 +118,7 @@ class IncomingVerificationPresenterTest {
|
|||
IncomingVerificationState.Step.Initial(
|
||||
deviceDisplayName = "a device name",
|
||||
deviceId = A_DEVICE_ID,
|
||||
formattedSignInTime = A_FORMATTED_DATE,
|
||||
formattedSignInTime = "567 TimeOrDate false",
|
||||
isWaiting = false,
|
||||
)
|
||||
)
|
||||
|
|
@ -178,7 +177,7 @@ class IncomingVerificationPresenterTest {
|
|||
IncomingVerificationState.Step.Initial(
|
||||
deviceDisplayName = "a device name",
|
||||
deviceId = A_DEVICE_ID,
|
||||
formattedSignInTime = A_FORMATTED_DATE,
|
||||
formattedSignInTime = "567 TimeOrDate false",
|
||||
isWaiting = false,
|
||||
)
|
||||
)
|
||||
|
|
@ -210,7 +209,7 @@ class IncomingVerificationPresenterTest {
|
|||
IncomingVerificationState.Step.Initial(
|
||||
deviceDisplayName = "a device name",
|
||||
deviceId = A_DEVICE_ID,
|
||||
formattedSignInTime = A_FORMATTED_DATE,
|
||||
formattedSignInTime = "567 TimeOrDate false",
|
||||
isWaiting = false,
|
||||
)
|
||||
)
|
||||
|
|
@ -281,7 +280,7 @@ class IncomingVerificationPresenterTest {
|
|||
sessionVerificationRequestDetails: SessionVerificationRequestDetails = aSessionVerificationRequestDetails,
|
||||
navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() },
|
||||
service: SessionVerificationService = FakeSessionVerificationService(),
|
||||
dateFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(A_FORMATTED_DATE),
|
||||
dateFormatter: DateFormatter = FakeDateFormatter(),
|
||||
) = IncomingVerificationPresenter(
|
||||
sessionVerificationRequestDetails = sessionVerificationRequestDetails,
|
||||
navigator = navigator,
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
|
||||
[versions]
|
||||
# Project
|
||||
android_gradle_plugin = "8.7.2"
|
||||
kotlin = "2.0.21"
|
||||
android_gradle_plugin = "8.7.3"
|
||||
kotlin = "2.1.0"
|
||||
kotlinpoet = "2.0.0"
|
||||
ksp = "2.0.21-1.0.28"
|
||||
ksp = "2.1.0-1.0.29"
|
||||
firebaseAppDistribution = "5.0.0"
|
||||
|
||||
# AndroidX
|
||||
|
|
@ -22,17 +22,17 @@ constraintlayout_compose = "1.1.0"
|
|||
lifecycle = "2.8.7"
|
||||
activity = "1.9.3"
|
||||
media3 = "1.5.0"
|
||||
camera = "1.4.0"
|
||||
camera = "1.4.1"
|
||||
|
||||
# Compose
|
||||
compose_bom = "2024.11.00"
|
||||
compose_bom = "2024.12.01"
|
||||
composecompiler = "1.5.15"
|
||||
|
||||
# Coroutines
|
||||
coroutines = "1.9.0"
|
||||
|
||||
# Accompanist
|
||||
accompanist = "0.36.0"
|
||||
accompanist = "0.37.0"
|
||||
|
||||
# Test
|
||||
test_core = "1.6.1"
|
||||
|
|
@ -50,10 +50,10 @@ wysiwyg = "2.37.14"
|
|||
telephoto = "0.14.0"
|
||||
|
||||
# Dependency analysis
|
||||
dependencyAnalysis = "2.5.0"
|
||||
dependencyAnalysis = "2.6.1"
|
||||
|
||||
# DI
|
||||
dagger = "2.53"
|
||||
dagger = "2.53.1"
|
||||
anvil = "0.4.0"
|
||||
|
||||
# Auto service
|
||||
|
|
@ -61,7 +61,7 @@ autoservice = "1.1.1"
|
|||
|
||||
# quality
|
||||
androidx-test-ext-junit = "1.2.1"
|
||||
kover = "0.8.3"
|
||||
kover = "0.9.0"
|
||||
|
||||
[libraries]
|
||||
# Project
|
||||
|
|
@ -150,7 +150,7 @@ test_arch_core = "androidx.arch.core:core-testing:2.2.0"
|
|||
test_junit = "junit:junit:4.13.2"
|
||||
test_runner = "androidx.test:runner:1.6.2"
|
||||
test_mockk = "io.mockk:mockk:1.13.13"
|
||||
test_konsist = "com.lemonappdev:konsist:0.17.1"
|
||||
test_konsist = "com.lemonappdev:konsist:0.17.3"
|
||||
test_turbine = "app.cash.turbine:turbine:1.2.0"
|
||||
test_truth = "com.google.truth:truth:1.4.4"
|
||||
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.18"
|
||||
|
|
@ -169,11 +169,11 @@ serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-jso
|
|||
kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8"
|
||||
showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
|
||||
showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
|
||||
jsoup = "org.jsoup:jsoup:1.18.1"
|
||||
jsoup = "org.jsoup:jsoup:1.18.3"
|
||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
|
||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.70"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.72"
|
||||
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
||||
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
||||
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||
|
|
@ -187,7 +187,7 @@ vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
|||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
||||
maplibre = "org.maplibre.gl:android-sdk:11.6.1"
|
||||
maplibre = "org.maplibre.gl:android-sdk:11.7.0"
|
||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
|
||||
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
|
||||
opusencoder = "io.element.android:opusencoder:1.1.0"
|
||||
|
|
@ -195,7 +195,7 @@ zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
|
|||
|
||||
# Analytics
|
||||
posthog = "com.posthog:posthog-android:3.9.3"
|
||||
sentry = "io.sentry:sentry-android:7.18.1"
|
||||
sentry = "io.sentry:sentry-android:7.19.0"
|
||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
|
||||
|
||||
|
|
|
|||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,7 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
distributionSha256Sum=f397b287023acdba1e9f6fc5ea72d22dd63669d59ed4a289a29b1a76eee151c6
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ package io.element.android.libraries.androidutils.browser
|
|||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Browser
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.browser.customtabs.CustomTabsSession
|
||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Open url in custom tab or, if not available, in the default browser.
|
||||
|
|
@ -51,6 +54,9 @@ fun Activity.openUrlInChromeCustomTab(
|
|||
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON", true)
|
||||
// Disable bookmark button
|
||||
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_START_BUTTON", true)
|
||||
intent.putExtra(Browser.EXTRA_HEADERS, Bundle().apply {
|
||||
putString("Accept-Language", Locale.getDefault().toLanguageTag())
|
||||
})
|
||||
}
|
||||
.launchUrl(this, Uri.parse(url))
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
package io.element.android.libraries.core.extensions
|
||||
|
||||
import java.text.Normalizer
|
||||
import java.util.Locale
|
||||
|
||||
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
||||
fun Boolean.to01() = if (this) "1" else "0"
|
||||
|
||||
|
|
@ -68,3 +71,21 @@ fun String.replacePrefix(oldPrefix: String, newPrefix: String): String {
|
|||
fun String.withBrackets(prefix: String = "(", suffix: String = ")"): String {
|
||||
return "$prefix$this$suffix"
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize the string.
|
||||
*/
|
||||
fun String.safeCapitalize(): String {
|
||||
return replaceFirstChar {
|
||||
if (it.isLowerCase()) {
|
||||
it.titlecase(Locale.getDefault())
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.withoutAccents(): String {
|
||||
return Normalizer.normalize(this, Normalizer.Form.NFD)
|
||||
.replace("\\p{Mn}+".toRegex(), "")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.api
|
||||
|
||||
interface DateFormatter {
|
||||
fun format(
|
||||
timestamp: Long?,
|
||||
mode: DateFormatterMode = DateFormatterMode.Full,
|
||||
useRelative: Boolean = false,
|
||||
): String
|
||||
}
|
||||
|
||||
enum class DateFormatterMode {
|
||||
/**
|
||||
* Full date and time.
|
||||
* Example:
|
||||
* "April 6, 1980 at 6:35 PM"
|
||||
* Format can be shorter when useRelative is true.
|
||||
* Example:
|
||||
* "6:35 PM"
|
||||
*/
|
||||
Full,
|
||||
|
||||
/**
|
||||
* Only month and year.
|
||||
* Example:
|
||||
* "April 1980"
|
||||
* "This month" can be returned when useRelative is true.
|
||||
* Example:
|
||||
* "This month"
|
||||
*/
|
||||
Month,
|
||||
|
||||
/**
|
||||
* Only day.
|
||||
* Example:
|
||||
* "Sunday 6 April"
|
||||
* "Today", "Yesterday" and day of week can be returned when useRelative is true.
|
||||
*/
|
||||
Day,
|
||||
|
||||
/**
|
||||
* Time if same day, else date.
|
||||
*/
|
||||
TimeOrDate,
|
||||
|
||||
/**
|
||||
* Only time whatever the day.
|
||||
*/
|
||||
TimeOnly,
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.api
|
||||
|
||||
interface DaySeparatorFormatter {
|
||||
fun format(timestamp: Long): String
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.api
|
||||
|
||||
fun interface LastMessageTimestampFormatter {
|
||||
fun format(timestamp: Long?): String
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import extension.setupAnvil
|
|||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
|
|
@ -16,15 +16,30 @@ setupAnvil()
|
|||
android {
|
||||
namespace = "io.element.android.libraries.dateformatter.impl"
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
api(projects.libraries.dateformatter.api)
|
||||
api(libs.datetime)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.safeCapitalize
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
interface DateFormatterDay {
|
||||
fun format(
|
||||
timestamp: Long,
|
||||
useRelative: Boolean,
|
||||
): String
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultDateFormatterDay @Inject constructor(
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
) : DateFormatterDay {
|
||||
override fun format(
|
||||
timestamp: Long,
|
||||
useRelative: Boolean,
|
||||
): String {
|
||||
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||
val today = localDateTimeProvider.providesNow()
|
||||
return if (useRelative) {
|
||||
val dayDiff = today.date.toEpochDays() - dateToFormat.date.toEpochDays()
|
||||
when (dayDiff) {
|
||||
0 -> dateFormatters.getRelativeDay(timestamp, "Today")
|
||||
1 -> dateFormatters.getRelativeDay(timestamp, "Yesterday")
|
||||
else -> if (dayDiff < 7) {
|
||||
dateFormatters.formatDateWithDay(dateToFormat)
|
||||
} else {
|
||||
if (today.year == dateToFormat.year) {
|
||||
dateFormatters.formatDateWithFullFormatNoYear(dateToFormat)
|
||||
} else {
|
||||
dateFormatters.formatDateWithFullFormat(dateToFormat)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (today.year == dateToFormat.year) {
|
||||
dateFormatters.formatDateWithFullFormatNoYear(dateToFormat)
|
||||
} else {
|
||||
dateFormatters.formatDateWithFullFormat(dateToFormat)
|
||||
}
|
||||
}
|
||||
.safeCapitalize()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class DateFormatterFull @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
private val dateFormatterDay: DateFormatterDay,
|
||||
) {
|
||||
fun format(
|
||||
timestamp: Long,
|
||||
useRelative: Boolean,
|
||||
): String {
|
||||
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||
val time = dateFormatters.formatTime(dateToFormat)
|
||||
return if (useRelative) {
|
||||
val now = localDateTimeProvider.providesNow()
|
||||
if (now.date == dateToFormat.date) {
|
||||
time
|
||||
} else {
|
||||
val dateStr = dateFormatterDay.format(timestamp, true)
|
||||
stringProvider.getString(R.string.common_date_date_at_time, dateStr, time)
|
||||
}
|
||||
} else {
|
||||
val dateStr = dateFormatters.formatDateWithFullFormat(dateToFormat)
|
||||
stringProvider.getString(R.string.common_date_date_at_time, dateStr, time)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import io.element.android.libraries.core.extensions.safeCapitalize
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class DateFormatterMonth @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
) {
|
||||
fun format(
|
||||
timestamp: Long,
|
||||
useRelative: Boolean,
|
||||
): String {
|
||||
val today = localDateTimeProvider.providesNow()
|
||||
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||
return if (useRelative && dateToFormat.month == today.month && dateToFormat.year == today.year) {
|
||||
stringProvider.getString(R.string.common_date_this_month)
|
||||
} else {
|
||||
dateFormatters.formatDateWithMonthAndYear(dateToFormat)
|
||||
}
|
||||
.safeCapitalize()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
|
|
@ -7,18 +7,16 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLastMessageTimestampFormatter @Inject constructor(
|
||||
class DateFormatterTime @Inject constructor(
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
) : LastMessageTimestampFormatter {
|
||||
override fun format(timestamp: Long?): String {
|
||||
if (timestamp == null) return ""
|
||||
) {
|
||||
fun format(
|
||||
timestamp: Long,
|
||||
useRelative: Boolean,
|
||||
): String {
|
||||
val currentDate = localDateTimeProvider.providesNow()
|
||||
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||
val isSameDay = currentDate.date == dateToFormat.date
|
||||
|
|
@ -30,7 +28,7 @@ class DefaultLastMessageTimestampFormatter @Inject constructor(
|
|||
dateFormatters.formatDate(
|
||||
dateToFormat = dateToFormat,
|
||||
currentDate = currentDate,
|
||||
useRelative = true
|
||||
useRelative = useRelative,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class DateFormatterTimeOnly @Inject constructor(
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
) {
|
||||
fun format(
|
||||
timestamp: Long,
|
||||
): String {
|
||||
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||
return dateFormatters.formatTime(dateToFormat)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,57 +7,64 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import android.text.format.DateUtils
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import kotlinx.datetime.toJavaLocalDateTime
|
||||
import timber.log.Timber
|
||||
import java.time.Period
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
class DateFormatters @Inject constructor(
|
||||
private val locale: Locale,
|
||||
localeChangeObserver: LocaleChangeObserver,
|
||||
private val clock: Clock,
|
||||
private val timeZoneProvider: TimezoneProvider,
|
||||
) {
|
||||
private val onlyTimeFormatter: DateTimeFormatter by lazy {
|
||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
|
||||
locale: Locale,
|
||||
) : LocaleChangeListener {
|
||||
init {
|
||||
localeChangeObserver.addListener(this)
|
||||
}
|
||||
|
||||
private val dateWithMonthFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") ?: "d MMM"
|
||||
DateTimeFormatter.ofPattern(pattern, locale)
|
||||
}
|
||||
private var dateTimeFormatters: DateTimeFormatters = DateTimeFormatters(locale)
|
||||
|
||||
private val dateWithYearFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") ?: "dd.MM.yyyy"
|
||||
DateTimeFormatter.ofPattern(pattern, locale)
|
||||
}
|
||||
|
||||
private val dateWithFullFormatFormatter: DateTimeFormatter by lazy {
|
||||
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(locale)
|
||||
override fun onLocaleChange() {
|
||||
Timber.w("Locale changed, updating formatters")
|
||||
dateTimeFormatters = DateTimeFormatters(Locale.getDefault())
|
||||
}
|
||||
|
||||
internal fun formatTime(localDateTime: LocalDateTime): String {
|
||||
return onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
return dateTimeFormatters.onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
internal fun formatDateWithMonthAndYear(localDateTime: LocalDateTime): String {
|
||||
return dateTimeFormatters.dateWithMonthAndYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
internal fun formatDateWithMonth(localDateTime: LocalDateTime): String {
|
||||
return dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
return dateTimeFormatters.dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
internal fun formatDateWithDay(localDateTime: LocalDateTime): String {
|
||||
return dateTimeFormatters.dateWithDayFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
internal fun formatDateWithYear(localDateTime: LocalDateTime): String {
|
||||
return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
return dateTimeFormatters.dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
internal fun formatDateWithFullFormat(localDateTime: LocalDateTime): String {
|
||||
return dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
return dateTimeFormatters.dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
internal fun formatDateWithFullFormatNoYear(localDateTime: LocalDateTime): String {
|
||||
return dateTimeFormatters.dateWithFullFormatNoYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
internal fun formatDate(
|
||||
|
|
@ -75,12 +82,12 @@ class DateFormatters @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getRelativeDay(ts: Long): String {
|
||||
internal fun getRelativeDay(ts: Long, default: String = ""): String {
|
||||
return DateUtils.getRelativeTimeSpanString(
|
||||
ts,
|
||||
clock.now().toEpochMilliseconds(),
|
||||
DateUtils.DAY_IN_MILLIS,
|
||||
DateUtils.FORMAT_SHOW_WEEKDAY
|
||||
)?.toString() ?: ""
|
||||
)?.toString() ?: default
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
class DateTimeFormatters(
|
||||
private val locale: Locale,
|
||||
) {
|
||||
val onlyTimeFormatter: DateTimeFormatter by lazy {
|
||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
|
||||
}
|
||||
|
||||
val dateWithMonthAndYearFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = bestDateTimePattern("MMMM YYYY")
|
||||
DateTimeFormatter.ofPattern(pattern, locale)
|
||||
}
|
||||
|
||||
val dateWithMonthFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = bestDateTimePattern("d MMM")
|
||||
DateTimeFormatter.ofPattern(pattern, locale)
|
||||
}
|
||||
|
||||
val dateWithDayFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = bestDateTimePattern("EEEE")
|
||||
DateTimeFormatter.ofPattern(pattern, locale)
|
||||
}
|
||||
|
||||
val dateWithYearFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = bestDateTimePattern("dd.MM.yyyy")
|
||||
DateTimeFormatter.ofPattern(pattern, locale)
|
||||
}
|
||||
|
||||
val dateWithFullFormatFormatter: DateTimeFormatter by lazy {
|
||||
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(locale)
|
||||
}
|
||||
|
||||
val dateWithFullFormatNoYearFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = DateFormat.getBestDateTimePattern(locale, "EEEE d MMMM") ?: "EEEE d MMMM"
|
||||
DateTimeFormatter.ofPattern(pattern, locale)
|
||||
}
|
||||
|
||||
private fun bestDateTimePattern(pattern: String): String {
|
||||
return DateFormat.getBestDateTimePattern(locale, pattern) ?: pattern
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultDateFormatter @Inject constructor(
|
||||
private val dateFormatterFull: DateFormatterFull,
|
||||
private val dateFormatterMonth: DateFormatterMonth,
|
||||
private val dateFormatterDay: DateFormatterDay,
|
||||
private val dateFormatterTime: DateFormatterTime,
|
||||
private val dateFormatterTimeOnly: DateFormatterTimeOnly,
|
||||
) : DateFormatter {
|
||||
override fun format(
|
||||
timestamp: Long?,
|
||||
mode: DateFormatterMode,
|
||||
useRelative: Boolean,
|
||||
): String {
|
||||
timestamp ?: return ""
|
||||
return when (mode) {
|
||||
DateFormatterMode.Full -> {
|
||||
dateFormatterFull.format(timestamp, useRelative)
|
||||
}
|
||||
DateFormatterMode.Month -> {
|
||||
dateFormatterMonth.format(timestamp, useRelative)
|
||||
}
|
||||
DateFormatterMode.Day -> {
|
||||
dateFormatterDay.format(timestamp, useRelative)
|
||||
}
|
||||
DateFormatterMode.TimeOrDate -> {
|
||||
dateFormatterTime.format(timestamp, useRelative)
|
||||
}
|
||||
DateFormatterMode.TimeOnly -> {
|
||||
dateFormatterTimeOnly.format(timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultDaySeparatorFormatter @Inject constructor(
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
) : DaySeparatorFormatter {
|
||||
override fun format(timestamp: Long): String {
|
||||
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||
// TODO use relative formatting once iOS uses it too
|
||||
return dateFormatters.formatDateWithFullFormat(dateToFormat)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import javax.inject.Inject
|
||||
|
||||
fun interface LocaleChangeObserver {
|
||||
fun addListener(listener: LocaleChangeListener)
|
||||
}
|
||||
|
||||
interface LocaleChangeListener {
|
||||
fun onLocaleChange()
|
||||
}
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLocaleChangeObserver @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : LocaleChangeObserver {
|
||||
init {
|
||||
registerReceiver(object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
listeners.forEach(LocaleChangeListener::onLocaleChange)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private val listeners = mutableSetOf<LocaleChangeListener>()
|
||||
|
||||
override fun addListener(listener: LocaleChangeListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
private fun registerReceiver(receiver: BroadcastReceiver) {
|
||||
val filter = IntentFilter()
|
||||
filter.addAction(Intent.ACTION_LOCALE_CHANGED)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
filter.addAction(Intent.ACTION_APPLICATION_LOCALE_CHANGED)
|
||||
}
|
||||
context.registerReceiver(receiver, filter)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl.previews
|
||||
|
||||
data class DateForPreview(
|
||||
val semantic: String,
|
||||
val date: String,
|
||||
)
|
||||
|
||||
val dateForPreviewToday = DateForPreview(
|
||||
semantic = "Today",
|
||||
date = "1980-04-06T18:35:24.00Z",
|
||||
)
|
||||
|
||||
val dateForPreviews = listOf(
|
||||
DateForPreview(
|
||||
semantic = "Now",
|
||||
date = dateForPreviewToday.date,
|
||||
),
|
||||
DateForPreview(
|
||||
semantic = "One second ago",
|
||||
date = "1980-04-06T18:35:23.00Z",
|
||||
),
|
||||
DateForPreview(
|
||||
semantic = "One minute ago",
|
||||
date = "1980-04-06T18:34:24.00Z",
|
||||
),
|
||||
DateForPreview(
|
||||
semantic = "One hour ago",
|
||||
date = "1980-04-06T17:35:24.00Z",
|
||||
),
|
||||
DateForPreview(
|
||||
semantic = "One day ago",
|
||||
date = "1980-04-05T18:35:24.00Z",
|
||||
),
|
||||
DateForPreview(
|
||||
semantic = "Two days ago",
|
||||
date = "1980-04-04T18:35:24.00Z",
|
||||
),
|
||||
DateForPreview(
|
||||
semantic = "One month ago",
|
||||
date = "1980-03-06T18:35:24.00Z",
|
||||
),
|
||||
DateForPreview(
|
||||
semantic = "One year ago",
|
||||
date = "1979-04-06T18:35:24.00Z",
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl.previews
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
|
||||
class DateFormatterModeProvider : PreviewParameterProvider<DateFormatterMode> {
|
||||
override val values: Sequence<DateFormatterMode>
|
||||
get() = DateFormatterMode.entries.asSequence()
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl.previews
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.utils.allBooleans
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun DateFormatterModeViewPreview(
|
||||
@PreviewParameter(DateFormatterModeProvider::class) dateFormatterMode: DateFormatterMode,
|
||||
) = ElementPreview {
|
||||
DateFormatterModeView(dateFormatterMode)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DateFormatterModeView(
|
||||
mode: DateFormatterMode,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val composeLocale = Locale.current
|
||||
val dateFormatter = remember {
|
||||
createFormatter(
|
||||
context = context,
|
||||
currentDate = dateForPreviewToday.date,
|
||||
locale = java.util.Locale.Builder()
|
||||
.setLanguageTag(composeLocale.toLanguageTag())
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = "Mode $mode / $composeLocale",
|
||||
style = ElementTheme.typography.fontHeadingSmMedium
|
||||
)
|
||||
val today = Instant.parse(dateForPreviewToday.date).toEpochMilliseconds()
|
||||
Text(
|
||||
text = "Today is: ${dateFormatter.format(today, DateFormatterMode.Full, useRelative = false)}",
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
)
|
||||
dateForPreviews.forEach { dateForPreview ->
|
||||
DateForPreviewItem(
|
||||
dateForPreview = dateForPreview,
|
||||
dateFormatter = dateFormatter,
|
||||
mode = mode,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DateForPreviewItem(
|
||||
dateForPreview: DateForPreview,
|
||||
dateFormatter: DefaultDateFormatter,
|
||||
mode: DateFormatterMode,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(2.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 8.dp),
|
||||
text = dateForPreview.semantic,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
val ts = Instant.parse(dateForPreview.date).toEpochMilliseconds()
|
||||
Row {
|
||||
Column {
|
||||
listOf("Absolute:", "Relative:").forEach { label ->
|
||||
Text(
|
||||
text = label,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column {
|
||||
allBooleans.forEach { useRelative ->
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = dateFormatter.format(ts, mode, useRelative),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl.previews
|
||||
|
||||
import android.content.Context
|
||||
import io.element.android.libraries.dateformatter.impl.DateFormatterFull
|
||||
import io.element.android.libraries.dateformatter.impl.DateFormatterMonth
|
||||
import io.element.android.libraries.dateformatter.impl.DateFormatterTime
|
||||
import io.element.android.libraries.dateformatter.impl.DateFormatterTimeOnly
|
||||
import io.element.android.libraries.dateformatter.impl.DateFormatters
|
||||
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
|
||||
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatterDay
|
||||
import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.TimeZone
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Create DefaultDateFormatter and set current time to the provided date.
|
||||
*/
|
||||
fun createFormatter(
|
||||
context: Context,
|
||||
currentDate: String,
|
||||
locale: Locale,
|
||||
): DefaultDateFormatter {
|
||||
val clock = PreviewClock().apply { givenInstant(Instant.parse(currentDate)) }
|
||||
val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
|
||||
val dateFormatters = DateFormatters(
|
||||
localeChangeObserver = {},
|
||||
clock = clock,
|
||||
timeZoneProvider = { TimeZone.UTC },
|
||||
locale = locale,
|
||||
)
|
||||
val stringProvider = PreviewStringProvider(context.resources)
|
||||
val dateFormatterDay = DefaultDateFormatterDay(
|
||||
localDateTimeProvider = localDateTimeProvider,
|
||||
dateFormatters = dateFormatters,
|
||||
)
|
||||
return DefaultDateFormatter(
|
||||
dateFormatterFull = DateFormatterFull(
|
||||
stringProvider = stringProvider,
|
||||
localDateTimeProvider = localDateTimeProvider,
|
||||
dateFormatters = dateFormatters,
|
||||
dateFormatterDay = dateFormatterDay,
|
||||
),
|
||||
dateFormatterMonth = DateFormatterMonth(
|
||||
stringProvider = stringProvider,
|
||||
localDateTimeProvider = localDateTimeProvider,
|
||||
dateFormatters = dateFormatters,
|
||||
),
|
||||
dateFormatterDay = dateFormatterDay,
|
||||
dateFormatterTime = DateFormatterTime(
|
||||
localDateTimeProvider = localDateTimeProvider,
|
||||
dateFormatters = dateFormatters,
|
||||
),
|
||||
dateFormatterTimeOnly = DateFormatterTimeOnly(
|
||||
localDateTimeProvider = localDateTimeProvider,
|
||||
dateFormatters = dateFormatters,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl.previews
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
class PreviewClock : Clock {
|
||||
private var instant: Instant = Instant.fromEpochMilliseconds(0)
|
||||
|
||||
fun givenInstant(instant: Instant) {
|
||||
this.instant = instant
|
||||
}
|
||||
|
||||
override fun now(): Instant = instant
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl.previews
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.annotation.StringRes
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
|
||||
class PreviewStringProvider(
|
||||
private val resources: Resources
|
||||
) : StringProvider {
|
||||
override fun getString(@StringRes resId: Int): String {
|
||||
return resources.getString(resId)
|
||||
}
|
||||
|
||||
override fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String {
|
||||
return resources.getString(resId, *formatArgs)
|
||||
}
|
||||
|
||||
override fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String {
|
||||
return resources.getQuantityString(resId, quantity, *formatArgs)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="common_date_date_at_time">"%1$s v %2$s"</string>
|
||||
<string name="common_date_this_month">"Tento měsíc"</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="common_date_this_month">"Αυτό το μήνα"</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="common_date_this_month">"Sel kuul"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="common_date_date_at_time">"%1$s à %2$s"</string>
|
||||
<string name="common_date_this_month">"Ce mois-ci"</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue