Merge branch 'develop' into feature/fga/some_room_related_fixes
This commit is contained in:
commit
40f3f2873b
109 changed files with 1395 additions and 460 deletions
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
maestro-cloud:
|
||||
name: Maestro test suite
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.review.state == 'approved'
|
||||
if: github.event.review.state == 'approved' || github.event_name == 'workflow_dispatch'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Allow one per PR.
|
||||
|
|
|
|||
67
.github/workflows/triage-labelled.yml
vendored
67
.github/workflows/triage-labelled.yml
vendored
|
|
@ -12,22 +12,10 @@ jobs:
|
|||
if: >
|
||||
github.repository == 'vector-im/element-x-android'
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||
query: |
|
||||
mutation add_to_project($projectid:ID!,$contentid:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
projectid: ${{ env.PROJECT_ID }}
|
||||
contentid: ${{ github.event.issue.node_id }}
|
||||
env:
|
||||
PROJECT_ID: "PVT_kwDOAM0swc4ABTXY"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/43
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
ex_plorers:
|
||||
name: Add labelled issues to X-Plorer project
|
||||
|
|
@ -35,23 +23,10 @@ jobs:
|
|||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'Team: Element X Feature')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||
query: |
|
||||
mutation add_to_project($projectid:ID!,$contentid:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
projectid: ${{ env.PROJECT_ID }}
|
||||
contentid: ${{ github.event.issue.node_id }}
|
||||
env:
|
||||
PROJECT_ID: "PVT_kwDOAM0swc4ALoFY"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/73
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
verticals_feature:
|
||||
name: Add labelled issues to Verticals Feature project
|
||||
|
|
@ -59,20 +34,18 @@ jobs:
|
|||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'Team: Verticals Feature')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||
query: |
|
||||
mutation add_to_project($projectid:ID!,$contentid:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
projectid: ${{ env.PROJECT_ID }}
|
||||
contentid: ${{ github.event.issue.node_id }}
|
||||
env:
|
||||
PROJECT_ID: "PVT_kwDOAM0swc4AHJKW"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/57
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
qa:
|
||||
name: Add labelled issues to QA project
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'Team: QA')
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
project-url: https://github.com/orgs/vector-im/projects/69
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
|
|
|||
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="1.8.10" />
|
||||
<option name="version" value="1.8.20" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
appId: ${APP_ID}
|
||||
---
|
||||
- tapOn: "Settings"
|
||||
- tapOn:
|
||||
id: "home_screen-settings"
|
||||
- tapOn: "Sign out"
|
||||
- takeScreenshot: build/maestro/900-SignOutDialg
|
||||
# Ensure cancel cancels
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
appId: ${APP_ID}
|
||||
---
|
||||
- tapOn: "Settings"
|
||||
- tapOn:
|
||||
id: "home_screen-settings"
|
||||
- assertVisible: "Rageshake to report bug"
|
||||
- takeScreenshot: build/maestro/600-Settings
|
||||
- tapOn:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.appnav
|
|||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
|
|
@ -133,8 +134,18 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
object RoomDetails : NavTarget
|
||||
}
|
||||
|
||||
private val timeline = inputs.room.timeline()
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
timeline.initialize()
|
||||
onDispose {
|
||||
timeline.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
|
|
|
|||
1
changelog.d/339.feature
Normal file
1
changelog.d/339.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Block & unblock users from room details screen.
|
||||
1
changelog.d/354.feature
Normal file
1
changelog.d/354.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Improve room list search and general UI
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_create_room_action_create_room">"Neuer Raum"</string>
|
||||
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Raumname"</string>
|
||||
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
|
||||
</resources>
|
||||
|
|
@ -3,6 +3,16 @@
|
|||
<string name="screen_create_room_action_create_room">"Cameră nouă"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Invitați persoane"</string>
|
||||
<string name="screen_create_room_add_people_title">"Adaugați persoane"</string>
|
||||
<string name="screen_create_room_error_creating_room">"A apărut o eroare la crearea camerei"</string>
|
||||
<string name="screen_create_room_private_option_description">"Mesajele din această cameră sunt criptate. Criptarea nu poate fi dezactivată ulterior."</string>
|
||||
<string name="screen_create_room_private_option_title">"Cameră privată (doar pe bază de invitație)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Mesajele nu sunt criptate și oricine le poate citi. Puteți activa criptarea la o dată ulterioară."</string>
|
||||
<string name="screen_create_room_public_option_title">"Cameră publică (oricine)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Numele camerei"</string>
|
||||
<string name="screen_create_room_room_name_placeholder">"e.g. Mici și Cozonaci"</string>
|
||||
<string name="screen_create_room_title">"Creați o cameră"</string>
|
||||
<string name="screen_create_room_topic_label">"Subiect (opțional)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Despre ce este această cameră?"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string>
|
||||
<string name="screen_start_chat_unknown_profile">"Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită."</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_decline_direct_chat_title">"Chat ablehnen"</string>
|
||||
<string name="screen_invites_empty_list">"Keine Einladungen"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s hat dich eingeladen"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s te invitó."</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s ti ha invitato"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_decline_chat_message">"Sigur doriți să refuzați alăturarea la %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Refuzați invitația"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Sigur doriți să refuzați conversațiile cu %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Refuzați conversația"</string>
|
||||
<string name="screen_invites_empty_list">"Nicio invitație"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s v-a invitat"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_change_server_subtitle">"Wie lautet die Adresse deines Servers?"</string>
|
||||
<string name="screen_login_title">"Willkommen zurück!"</string>
|
||||
<string name="screen_login_password_hint">"Passwort"</string>
|
||||
<string name="screen_login_username_hint">"Benutzername"</string>
|
||||
</resources>
|
||||
|
|
@ -4,17 +4,17 @@
|
|||
<string name="screen_change_server_error_no_sliding_sync_message">"Este servidor no soporta sliding sync."</string>
|
||||
<string name="screen_change_server_form_header">"Dirección del homeserver"</string>
|
||||
<string name="screen_change_server_form_notice">"Solo puedes conectarte a un servidor que soporte sliding sync. El administrador de tu servidor tendrá que configurarlo. %1$s"</string>
|
||||
<string name="screen_change_server_submit">"Continuar"</string>
|
||||
<string name="screen_change_server_subtitle">"¿Cuál es la dirección de tu servidor?"</string>
|
||||
<string name="screen_change_server_title">"Selecciona tu servidor"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Esta cuenta ha sido desactivada."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Usuario y/o contraseña incorrectos"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Este no es un id de usuario válido. Formato esperado: \'@user:homeserver.org\'"</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"El servidor seleccionado no admite contraseñas ni inicio de sesión OIDC. Póngase en contacto con su administrador o elija otro homeserver."</string>
|
||||
<string name="screen_login_form_header">"Introduce tus datos"</string>
|
||||
<string name="screen_login_password_hint">"Contraseña"</string>
|
||||
<string name="screen_login_server_header">"Donde viven tus conversaciones"</string>
|
||||
<string name="screen_login_submit">"Continuar"</string>
|
||||
<string name="screen_login_title">"¡Hola de nuevo!"</string>
|
||||
<string name="screen_change_server_submit">"Continuar"</string>
|
||||
<string name="screen_change_server_title">"Selecciona tu servidor"</string>
|
||||
<string name="screen_login_password_hint">"Contraseña"</string>
|
||||
<string name="screen_login_submit">"Continuar"</string>
|
||||
<string name="screen_login_username_hint">"Usuario"</string>
|
||||
</resources>
|
||||
|
|
@ -4,17 +4,17 @@
|
|||
<string name="screen_change_server_error_no_sliding_sync_message">"Questo server attualmente non supporta la sincronizzazione scorrevole."</string>
|
||||
<string name="screen_change_server_form_header">"URL dell\'homeserver"</string>
|
||||
<string name="screen_change_server_form_notice">"Puoi connetterti solo a un server esistente che supporta la sincronizzazione scorrevole. L\'amministratore del tuo server domestico dovrà configurarlo. %1$s"</string>
|
||||
<string name="screen_change_server_submit">"Continua"</string>
|
||||
<string name="screen_change_server_subtitle">"Qual è l\'indirizzo del tuo server?"</string>
|
||||
<string name="screen_change_server_title">"Seleziona il tuo server"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Questo profilo è stato disattivato."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Nome utente e/o password errati"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Questo non è un identificatore utente valido. Formato previsto: \'@user:homeserver.org\'"</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"L\'homeserver selezionato non supporta la password o l\'accesso OIDC. Contatta il tuo amministratore o scegli un altro homeserver."</string>
|
||||
<string name="screen_login_form_header">"Inserisci i tuoi dati"</string>
|
||||
<string name="screen_login_password_hint">"Password"</string>
|
||||
<string name="screen_login_server_header">"Dove vivono le tue conversazioni"</string>
|
||||
<string name="screen_login_submit">"Continua"</string>
|
||||
<string name="screen_login_title">"Bentornato!"</string>
|
||||
<string name="screen_change_server_submit">"Continua"</string>
|
||||
<string name="screen_change_server_title">"Seleziona il tuo server"</string>
|
||||
<string name="screen_login_password_hint">"Password"</string>
|
||||
<string name="screen_login_submit">"Continua"</string>
|
||||
<string name="screen_login_username_hint">"Nome utente"</string>
|
||||
</resources>
|
||||
|
|
@ -4,17 +4,17 @@
|
|||
<string name="screen_change_server_error_no_sliding_sync_message">"Momentan acest server nu oferă suport pentru sliding sync."</string>
|
||||
<string name="screen_change_server_form_header">"Adresa URL a homeserver-ului"</string>
|
||||
<string name="screen_change_server_form_notice">"Vă putețo conecta numai la un server existent care oferă suport pentru sliding sync. Administratorul homeserver-ului dumneavoastră va trebui să îl configureze. %1$s"</string>
|
||||
<string name="screen_change_server_submit">"Continuați"</string>
|
||||
<string name="screen_change_server_subtitle">"Care este adresa serverului dumneavoastră?"</string>
|
||||
<string name="screen_change_server_title">"Selectați serverul"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Acest cont a fost dezactivat."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Utilizator și/sau parolă incorecte"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Acesta nu este un identificator de utilizator valid. Format așteptat: „@user:homeserver.org”"</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"Homeserver-ul selectat nu acceptă autentificarea prin parola sau OIDC. Te rugăm să contactezi administratorul sau să alegi un alt homeserver."</string>
|
||||
<string name="screen_login_form_header">"Introduceți detaliile"</string>
|
||||
<string name="screen_login_password_hint">"Parolă"</string>
|
||||
<string name="screen_login_server_header">"Locul unde trăiesc conversațiile tale"</string>
|
||||
<string name="screen_login_submit">"Continuați"</string>
|
||||
<string name="screen_login_title">"Bine ați revenit!"</string>
|
||||
<string name="screen_change_server_submit">"Continuați"</string>
|
||||
<string name="screen_change_server_title">"Selectați serverul"</string>
|
||||
<string name="screen_login_password_hint">"Parola"</string>
|
||||
<string name="screen_login_submit">"Continuați"</string>
|
||||
<string name="screen_login_username_hint">"Utilizator"</string>
|
||||
</resources>
|
||||
|
|
@ -4,17 +4,17 @@
|
|||
<string name="screen_change_server_error_no_sliding_sync_message">"This server currently doesn’t support sliding sync."</string>
|
||||
<string name="screen_change_server_form_header">"Homeserver URL"</string>
|
||||
<string name="screen_change_server_form_notice">"You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$s"</string>
|
||||
<string name="screen_change_server_submit">"Continue"</string>
|
||||
<string name="screen_change_server_subtitle">"What is the address of your server?"</string>
|
||||
<string name="screen_change_server_title">"Select your server"</string>
|
||||
<string name="screen_login_error_deactivated_account">"This account has been deactivated."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Incorrect username and/or password"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"This is not a valid user identifier. Expected format: ‘@user:homeserver.org’"</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"The selected homeserver doesn\'t support password or OIDC login. Please contact your admin or choose another homeserver."</string>
|
||||
<string name="screen_login_form_header">"Enter your details"</string>
|
||||
<string name="screen_login_password_hint">"Password"</string>
|
||||
<string name="screen_login_server_header">"Where your conversations live"</string>
|
||||
<string name="screen_login_submit">"Continue"</string>
|
||||
<string name="screen_login_title">"Welcome back!"</string>
|
||||
<string name="screen_change_server_submit">"Continue"</string>
|
||||
<string name="screen_change_server_title">"Select your server"</string>
|
||||
<string name="screen_login_password_hint">"Password"</string>
|
||||
<string name="screen_login_submit">"Continue"</string>
|
||||
<string name="screen_login_username_hint">"Username"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_signout_confirmation_dialog_submit">"Abmelden"</string>
|
||||
<string name="screen_signout_confirmation_dialog_title">"Abmelden"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"Abmeldung läuft…"</string>
|
||||
<string name="screen_signout_preference_item">"Abmelden"</string>
|
||||
</resources>
|
||||
|
|
@ -80,13 +80,6 @@ class TimelinePresenter @Inject constructor(
|
|||
.launchIn(this)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
timeline.initialize()
|
||||
onDispose {
|
||||
timeline.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
return TimelineState(
|
||||
highlightedEventId = highlightedEventId.value,
|
||||
paginationState = paginationState.value,
|
||||
|
|
|
|||
|
|
@ -49,23 +49,6 @@ class TimelinePresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - makes sure timeline is initialized and disposed`() = runTest {
|
||||
val fakeTimeline = FakeMatrixTimeline()
|
||||
val presenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
room = FakeMatrixRoom(matrixTimeline = fakeTimeline),
|
||||
)
|
||||
assertThat(fakeTimeline.isInitialized).isFalse()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(2)
|
||||
assertThat(fakeTimeline.isInitialized).isTrue()
|
||||
}
|
||||
assertThat(fakeTimeline.isInitialized).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - load more`() = runTest {
|
||||
val presenter = TimelinePresenter(
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ class NetworkMonitorImpl @Inject constructor(
|
|||
|
||||
private fun listenToConnectionChanges() {
|
||||
val request = NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
// .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
// .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_onboarding_welcome_title">"Sei in deinem Element"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bug_report_editor_placeholder">"Beschreibe den Fehler…"</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Absturzprotokolle senden"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Bildschirmfoto senden"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.blockuser
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Block
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||
import io.element.android.libraries.designsystem.theme.LocalColors
|
||||
|
||||
@Composable
|
||||
internal fun BlockUserSection(state: RoomMemberDetailsState, modifier: Modifier = Modifier) {
|
||||
PreferenceCategory(showDivider = false, modifier = modifier) {
|
||||
if (state.isBlocked) {
|
||||
PreferenceText(
|
||||
title = stringResource(R.string.screen_dm_details_unblock_user),
|
||||
icon = Icons.Outlined.Block,
|
||||
onClick = { state.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) },
|
||||
)
|
||||
} else {
|
||||
PreferenceText(
|
||||
title = stringResource(R.string.screen_dm_details_block_user),
|
||||
icon = Icons.Outlined.Block,
|
||||
tintColor = LocalColors.current.textActionCritical,
|
||||
onClick = { state.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun BlockUserDialogs(state: RoomMemberDetailsState) {
|
||||
when (state.displayConfirmationDialog) {
|
||||
null -> Unit
|
||||
RoomMemberDetailsState.ConfirmationDialog.Block -> {
|
||||
BlockConfirmationDialog(
|
||||
onBlockAction = { state.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) },
|
||||
onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) }
|
||||
)
|
||||
}
|
||||
RoomMemberDetailsState.ConfirmationDialog.Unblock -> {
|
||||
UnblockConfirmationDialog(
|
||||
onUnblockAction = { state.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) },
|
||||
onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun BlockConfirmationDialog(onBlockAction: () -> Unit, onDismiss: () -> Unit) {
|
||||
ConfirmationDialog(
|
||||
content = stringResource(R.string.screen_dm_details_block_alert_description),
|
||||
submitText = stringResource(R.string.screen_dm_details_block_alert_action),
|
||||
onSubmitClicked = onBlockAction,
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun UnblockConfirmationDialog(onUnblockAction: () -> Unit, onDismiss: () -> Unit) {
|
||||
ConfirmationDialog(
|
||||
content = stringResource(R.string.screen_dm_details_unblock_alert_description),
|
||||
submitText = stringResource(R.string.screen_dm_details_unblock_alert_action),
|
||||
onSubmitClicked = onUnblockAction,
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
|
@ -63,7 +63,10 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
activityResultLauncher = null,
|
||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||
text = permalink,
|
||||
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found)
|
||||
)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,12 +89,21 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
override fun View(modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
val state = presenter.present()
|
||||
|
||||
fun onShareRoom() {
|
||||
this.onShareRoom(context)
|
||||
}
|
||||
|
||||
fun onShareMember(roomMember: RoomMember) {
|
||||
this.onShareMember(context, roomMember)
|
||||
}
|
||||
|
||||
RoomDetailsView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
goBack = { navigateUp() },
|
||||
onShareRoom = { onShareRoom(context) },
|
||||
onShareMember = { onShareMember(context, it) },
|
||||
goBack = this::navigateUp,
|
||||
onShareRoom = ::onShareRoom,
|
||||
onShareMember = ::onShareMember,
|
||||
openRoomMemberList = ::openRoomMemberList,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
|
|
@ -41,6 +42,7 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
private val room: MatrixRoom,
|
||||
private val roomMembershipObserver: RoomMembershipObserver,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory,
|
||||
) : Presenter<RoomDetailsState> {
|
||||
|
||||
@Composable
|
||||
|
|
@ -57,6 +59,8 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
val dmMemberState by room.getDmMemberFlow()
|
||||
.collectAsState(initial = null, context = coroutineDispatchers.computation)
|
||||
|
||||
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMemberState)
|
||||
|
||||
val roomType = getRoomType(dmMemberState)
|
||||
|
||||
fun handleEvents(event: RoomDetailsEvent) {
|
||||
|
|
@ -74,6 +78,8 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val roomMemberDetailsState = roomMemberDetailsPresenter?.present()
|
||||
|
||||
return RoomDetailsState(
|
||||
roomId = room.roomId.value,
|
||||
roomName = room.name ?: room.displayName,
|
||||
|
|
@ -85,10 +91,18 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
displayLeaveRoomWarning = leaveRoomWarning.value,
|
||||
error = error.value,
|
||||
roomType = roomType.value,
|
||||
roomMemberDetailsState = roomMemberDetailsState,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun roomMemberDetailsPresenter(dmMemberState: RoomMember?) = remember(dmMemberState) {
|
||||
dmMemberState?.let { roomMember ->
|
||||
roomMembersDetailsPresenterFactory.create(roomMember)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getRoomType(dmMember: RoomMember?): State<RoomDetailsType> = remember(dmMember) {
|
||||
derivedStateOf {
|
||||
|
|
|
|||
|
|
@ -16,10 +16,8 @@
|
|||
|
||||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.isLoading
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
data class RoomDetailsState(
|
||||
|
|
@ -33,6 +31,7 @@ data class RoomDetailsState(
|
|||
val displayLeaveRoomWarning: LeaveRoomWarning?,
|
||||
val error: RoomDetailsError?,
|
||||
val roomType: RoomDetailsType,
|
||||
val roomMemberDetailsState: RoomMemberDetailsState?,
|
||||
val eventSink: (RoomDetailsEvent) -> Unit
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -71,5 +71,6 @@ fun aRoomDetailsState() = RoomDetailsState(
|
|||
displayLeaveRoomWarning = null,
|
||||
error = null,
|
||||
roomType = RoomDetailsType.Room,
|
||||
roomMemberDetailsState = null,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ import androidx.compose.ui.res.vectorResource
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomdetails.impl.members.details.BlockSection
|
||||
import io.element.android.features.roomdetails.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.roomdetails.blockuser.BlockUserSection
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberShareSection
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
|
@ -135,10 +136,11 @@ fun RoomDetailsView(
|
|||
})
|
||||
}
|
||||
is RoomDetailsType.Dm -> {
|
||||
BlockSection(
|
||||
isBlocked = state.roomType.roomMember.isIgnored,
|
||||
onToggleBlock = { /*TODO*/ }
|
||||
)
|
||||
if (state.roomMemberDetailsState != null) {
|
||||
val roomMemberState = state.roomMemberDetailsState
|
||||
BlockUserSection(roomMemberState)
|
||||
BlockUserDialogs(roomMemberState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import com.squareup.anvil.annotations.ContributesTo
|
|||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
||||
import io.element.android.features.roomdetails.impl.members.RoomUserListDataSource
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||
import io.element.android.features.userlist.api.UserListDataSource
|
||||
|
|
@ -28,7 +27,6 @@ import io.element.android.libraries.di.RoomScope
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import javax.inject.Named
|
||||
|
||||
@Module
|
||||
|
|
@ -46,11 +44,12 @@ object RoomMemberProvidesModule {
|
|||
|
||||
@Provides
|
||||
fun provideRoomMemberDetailsPresenterFactory(
|
||||
matrixClient: MatrixClient,
|
||||
room: MatrixRoom,
|
||||
): RoomMemberDetailsPresenter.Factory {
|
||||
return object : RoomMemberDetailsPresenter.Factory {
|
||||
override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter {
|
||||
return RoomMemberDetailsPresenter(room, roomMember)
|
||||
return RoomMemberDetailsPresenter(matrixClient.sessionId, room, roomMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,14 +26,12 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class RoomMemberListNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val room: MatrixRoom,
|
||||
private val presenter: RoomMemberListPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ import io.element.android.libraries.matrix.api.room.roomMembers
|
|||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.coroutines.flow.dropWhile
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.skip
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -40,7 +38,7 @@ class RoomUserListDataSource @Inject constructor(
|
|||
|
||||
override suspend fun search(query: String): List<MatrixUser> = withContext(coroutineDispatchers.io) {
|
||||
val roomMembers = room.membersStateFlow
|
||||
.dropWhile { it !is MatrixRoomMembersState.Ready}
|
||||
.dropWhile { it !is MatrixRoomMembersState.Ready }
|
||||
.first()
|
||||
.roomMembers()
|
||||
val filteredMembers = if (query.isBlank()) {
|
||||
|
|
|
|||
|
|
@ -16,4 +16,8 @@
|
|||
|
||||
package io.element.android.features.roomdetails.impl.members.details
|
||||
|
||||
sealed interface RoomMemberDetailsEvents
|
||||
sealed interface RoomMemberDetailsEvents {
|
||||
data class BlockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents
|
||||
data class UnblockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents
|
||||
object ClearConfirmationDialog : RoomMemberDetailsEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten
|
|||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import timber.log.Timber
|
||||
|
|
@ -52,7 +51,6 @@ class RoomMemberDetailsNode @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
fun onShareUser() {
|
||||
|
|
|
|||
|
|
@ -17,15 +17,26 @@
|
|||
package io.element.android.features.roomdetails.impl.members.details
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState.ConfirmationDialog
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||
private val currentUserSessionId: SessionId,
|
||||
private val room: MatrixRoom,
|
||||
@Assisted private val roomMember: RoomMember,
|
||||
) : Presenter<RoomMemberDetailsState> {
|
||||
|
|
@ -36,11 +47,31 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun present(): RoomMemberDetailsState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
|
||||
val isBlocked = remember { mutableStateOf(roomMember.isIgnored) }
|
||||
|
||||
// fun handleEvents(event: RoomMemberDetailsEvents) {
|
||||
// when (event) {
|
||||
// }
|
||||
// }
|
||||
fun handleEvents(event: RoomMemberDetailsEvents) {
|
||||
when (event) {
|
||||
is RoomMemberDetailsEvents.BlockUser -> {
|
||||
if (event.needsConfirmation) {
|
||||
confirmationDialog = ConfirmationDialog.Block
|
||||
} else {
|
||||
confirmationDialog = null
|
||||
coroutineScope.blockUser(roomMember.userId, isBlocked)
|
||||
}
|
||||
}
|
||||
is RoomMemberDetailsEvents.UnblockUser -> {
|
||||
if (event.needsConfirmation) {
|
||||
confirmationDialog = ConfirmationDialog.Unblock
|
||||
} else {
|
||||
confirmationDialog = null
|
||||
coroutineScope.unblockUser(roomMember.userId, isBlocked)
|
||||
}
|
||||
}
|
||||
RoomMemberDetailsEvents.ClearConfirmationDialog -> confirmationDialog = null
|
||||
}
|
||||
}
|
||||
|
||||
val userName by produceState(initialValue = roomMember.displayName) {
|
||||
room.userDisplayName(roomMember.userId).onSuccess { displayName ->
|
||||
|
|
@ -58,8 +89,18 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
|||
userId = roomMember.userId.value,
|
||||
userName = userName,
|
||||
avatarUrl = userAvatar,
|
||||
isBlocked = roomMember.isIgnored,
|
||||
// eventSink = ::handleEvents
|
||||
isBlocked = isBlocked.value,
|
||||
displayConfirmationDialog = confirmationDialog,
|
||||
isCurrentUser = roomMember.userId == currentUserSessionId,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState<Boolean>) = launch {
|
||||
room.ignoreUser(userId).onSuccess { isBlockedState.value = true }
|
||||
}
|
||||
|
||||
private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState<Boolean>) = launch {
|
||||
room.unignoreUser(userId).onSuccess { isBlockedState.value = false }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,5 +21,11 @@ data class RoomMemberDetailsState(
|
|||
val userName: String?,
|
||||
val avatarUrl: String?,
|
||||
val isBlocked: Boolean,
|
||||
// val eventSink: (RoomMemberDetailsEvents) -> Unit
|
||||
)
|
||||
val displayConfirmationDialog: ConfirmationDialog? = null,
|
||||
val isCurrentUser: Boolean,
|
||||
val eventSink: (RoomMemberDetailsEvents) -> Unit
|
||||
) {
|
||||
enum class ConfirmationDialog {
|
||||
Block, Unblock
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ open class RoomMemberDetailsStateProvider : PreviewParameterProvider<RoomMemberD
|
|||
aRoomMemberDetailsState(),
|
||||
aRoomMemberDetailsState().copy(userName = null),
|
||||
aRoomMemberDetailsState().copy(isBlocked = true),
|
||||
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block),
|
||||
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock),
|
||||
// Add other states here
|
||||
)
|
||||
}
|
||||
|
|
@ -33,5 +35,6 @@ fun aRoomMemberDetailsState() = RoomMemberDetailsState(
|
|||
userName = "Daniel",
|
||||
avatarUrl = null,
|
||||
isBlocked = false,
|
||||
// eventSink = {},
|
||||
isCurrentUser = false,
|
||||
eventSink = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,12 +39,15 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomdetails.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.roomdetails.blockuser.BlockUserSection
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
|
|
@ -86,9 +89,10 @@ fun RoomMemberDetailsView(
|
|||
// TODO implement send DM
|
||||
})
|
||||
|
||||
BlockSection(isBlocked = state.isBlocked, onToggleBlock = {
|
||||
// TODO implement block & unblock
|
||||
})
|
||||
if (!state.isCurrentUser) {
|
||||
BlockUserSection(state)
|
||||
BlockUserDialogs(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -139,24 +143,6 @@ internal fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier =
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun BlockSection(isBlocked: Boolean, onToggleBlock: () -> Unit, modifier: Modifier = Modifier) {
|
||||
PreferenceCategory(showDivider = false, modifier = modifier) {
|
||||
if (isBlocked) {
|
||||
PreferenceText(
|
||||
title = stringResource(R.string.screen_dm_details_unblock_user),
|
||||
icon = Icons.Outlined.Block,
|
||||
)
|
||||
} else {
|
||||
PreferenceText(
|
||||
title = stringResource(R.string.screen_dm_details_block_user),
|
||||
icon = Icons.Outlined.Block,
|
||||
tintColor = LocalColors.current.textActionCritical,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"1 Person"</item>
|
||||
<item quantity="other">"%1$d Personen"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_share_room_title">"Raum teilen"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Blockieren"</string>
|
||||
<string name="screen_dm_details_block_user">"Nutzer blockieren"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Blockierung aufheben"</string>
|
||||
<string name="screen_dm_details_unblock_user">"Nutzer entblockieren"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Raum verlassen"</string>
|
||||
<string name="screen_room_details_security_title">"Sicherheit"</string>
|
||||
<string name="screen_room_details_topic_title">"Thema"</string>
|
||||
</resources>
|
||||
|
|
@ -4,18 +4,18 @@
|
|||
<item quantity="one">"Una persona"</item>
|
||||
<item quantity="other">"%1$d personas"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Cifrado de mensajes activado"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invitar a otras personas"</string>
|
||||
<string name="screen_room_details_share_room_title">"Compartir sala"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Bloquear"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puedes revertir esta acción en cualquier momento."</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento."</string>
|
||||
<string name="screen_dm_details_block_user">"Bloquear usuario"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Desbloquear"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"Al desbloquear al usuario, podrás volver a ver todos sus mensajes."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Desbloquear usuario"</string>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Cifrado de mensajes activado"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invitar a otras personas"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Salir de la sala"</string>
|
||||
<string name="screen_room_details_people_title">"Personas"</string>
|
||||
<string name="screen_room_details_security_title">"Seguridad"</string>
|
||||
<string name="screen_room_details_share_room_title">"Compartir sala"</string>
|
||||
<string name="screen_room_details_topic_title">"Tema"</string>
|
||||
</resources>
|
||||
|
|
@ -4,18 +4,18 @@
|
|||
<item quantity="one">"1 persona"</item>
|
||||
<item quantity="other">"%1$d persone"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Crittografia messaggi abilitata"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invita persone"</string>
|
||||
<string name="screen_room_details_share_room_title">"Condividi stanza"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Blocca"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti i loro messaggi saranno nascosti. Potrai annullare questa azione in qualsiasi momento."</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento."</string>
|
||||
<string name="screen_dm_details_block_user">"Blocca utente"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Sblocca"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Sblocca utente"</string>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Crittografia messaggi abilitata"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invita persone"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Esci dalla stanza"</string>
|
||||
<string name="screen_room_details_people_title">"Persone"</string>
|
||||
<string name="screen_room_details_security_title">"Sicurezza"</string>
|
||||
<string name="screen_room_details_share_room_title">"Condividi stanza"</string>
|
||||
<string name="screen_room_details_topic_title">"Oggetto"</string>
|
||||
</resources>
|
||||
|
|
@ -5,18 +5,18 @@
|
|||
<item quantity="few"></item>
|
||||
<item quantity="other">"%1$d persoane"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Criptarea mesajelor este activată"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invitați persoane"</string>
|
||||
<string name="screen_room_details_share_room_title">"Partajați camera"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Blocați"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."</string>
|
||||
<string name="screen_dm_details_block_user">"Blocați utilizatorul"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Deblocați"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Deblocați utilizatorul"</string>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Criptarea mesajelor este activată"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invitați persoane"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Părăsiți camera"</string>
|
||||
<string name="screen_room_details_people_title">"Persoane"</string>
|
||||
<string name="screen_room_details_security_title">"Securitate"</string>
|
||||
<string name="screen_room_details_share_room_title">"Partajați camera"</string>
|
||||
<string name="screen_room_details_topic_title">"Subiect"</string>
|
||||
</resources>
|
||||
|
|
@ -4,18 +4,18 @@
|
|||
<item quantity="one">"1 person"</item>
|
||||
<item quantity="other">"%1$d people"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invite people"</string>
|
||||
<string name="screen_room_details_share_room_title">"Share room"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Block"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
|
||||
<string name="screen_dm_details_block_user">"Block user"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Unblock"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"On unblocking the user, you will be able to see all messages by them again."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Unblock user"</string>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invite people"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Leave room"</string>
|
||||
<string name="screen_room_details_people_title">"People"</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_topic_title">"Topic"</string>
|
||||
</resources>
|
||||
|
|
@ -23,9 +23,12 @@ import com.google.common.truth.Truth
|
|||
import io.element.android.features.roomdetails.impl.LeaveRoomWarning
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsType
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
|
|
@ -33,7 +36,9 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -49,10 +54,19 @@ class RoomDetailsPresenterTests {
|
|||
private val roomMembershipObserver = RoomMembershipObserver()
|
||||
private val testCoroutineDispatchers = testCoroutineDispatchers()
|
||||
|
||||
private fun aRoomDetailsPresenter(room: MatrixRoom): RoomDetailsPresenter {
|
||||
val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
|
||||
override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter {
|
||||
return RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||
}
|
||||
}
|
||||
return RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers, roomMemberDetailsPresenterFactory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state is created from room info`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -71,7 +85,7 @@ class RoomDetailsPresenterTests {
|
|||
@Test
|
||||
fun `present - room member count is calculated asynchronously`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -86,7 +100,7 @@ class RoomDetailsPresenterTests {
|
|||
@Test
|
||||
fun `present - initial state with no room name`() = runTest {
|
||||
val room = aMatrixRoom(name = null)
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -97,12 +111,41 @@ class RoomDetailsPresenterTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state with DM member sets custom DM roomType`() = runTest {
|
||||
val room = aMatrixRoom(
|
||||
isEncrypted = true,
|
||||
isPublic = false,
|
||||
name = null
|
||||
).apply {
|
||||
val roomMembers = listOf(
|
||||
aRoomMember(A_SESSION_ID),
|
||||
aRoomMember(A_USER_ID_2),
|
||||
)
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
|
||||
}
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
// It's not configured yet in the first iteration
|
||||
Truth.assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Room)
|
||||
|
||||
// Once updated, the RoomDetailsType becomes 'Dm'
|
||||
val updatedState = awaitItem()
|
||||
Truth.assertThat(updatedState.roomType).isEqualTo(RoomDetailsType.Dm(aRoomMember()))
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - can handle error while fetching member count`() = runTest {
|
||||
val room = aMatrixRoom(name = null).apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Error(Throwable()))
|
||||
}
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -116,7 +159,7 @@ class RoomDetailsPresenterTests {
|
|||
val room = aMatrixRoom(isPublic = false).apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
|
||||
}
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -132,7 +175,7 @@ class RoomDetailsPresenterTests {
|
|||
val room = aMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(aRoomMember())))
|
||||
}
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -148,7 +191,7 @@ class RoomDetailsPresenterTests {
|
|||
val room = aMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
|
||||
}
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -164,7 +207,7 @@ class RoomDetailsPresenterTests {
|
|||
val room = aMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
|
||||
}
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -185,7 +228,7 @@ class RoomDetailsPresenterTests {
|
|||
val room = aMatrixRoom().apply {
|
||||
givenLeaveRoomError(Throwable())
|
||||
}
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth
|
||||
import io.element.android.features.roomdetails.aMatrixRoom
|
||||
import io.element.android.features.roomdetails.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -37,7 +40,7 @@ class RoomMemberDetailsPresenterTests {
|
|||
givenUserAvatarUrlResult(Result.success("A custom avatar"))
|
||||
}
|
||||
val roomMember = aRoomMember(displayName = "Alice")
|
||||
val presenter = RoomMemberDetailsPresenter(room, roomMember)
|
||||
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -60,7 +63,7 @@ class RoomMemberDetailsPresenterTests {
|
|||
givenUserAvatarUrlResult(Result.failure(Throwable()))
|
||||
}
|
||||
val roomMember = aRoomMember(displayName = "Alice")
|
||||
val presenter = RoomMemberDetailsPresenter(room, roomMember)
|
||||
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -79,7 +82,7 @@ class RoomMemberDetailsPresenterTests {
|
|||
givenUserAvatarUrlResult(Result.success(null))
|
||||
}
|
||||
val roomMember = aRoomMember(displayName = "Alice")
|
||||
val presenter = RoomMemberDetailsPresenter(room, roomMember)
|
||||
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -90,4 +93,63 @@ class RoomMemberDetailsPresenterTests {
|
|||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val roomMember = aRoomMember()
|
||||
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true))
|
||||
|
||||
val dialogState = awaitItem()
|
||||
Truth.assertThat(dialogState.displayConfirmationDialog).isEqualTo(RoomMemberDetailsState.ConfirmationDialog.Block)
|
||||
|
||||
dialogState.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog)
|
||||
Truth.assertThat(awaitItem().displayConfirmationDialog).isNull()
|
||||
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val roomMember = aRoomMember()
|
||||
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false))
|
||||
Truth.assertThat(awaitItem().isBlocked).isTrue()
|
||||
|
||||
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false))
|
||||
Truth.assertThat(awaitItem().isBlocked).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val roomMember = aRoomMember()
|
||||
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true))
|
||||
|
||||
val dialogState = awaitItem()
|
||||
Truth.assertThat(dialogState.displayConfirmationDialog).isEqualTo(RoomMemberDetailsState.ConfirmationDialog.Unblock)
|
||||
|
||||
dialogState.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog)
|
||||
Truth.assertThat(awaitItem().displayConfirmationDialog).isNull()
|
||||
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,4 +20,5 @@ sealed interface RoomListEvents {
|
|||
data class UpdateFilter(val newFilter: String) : RoomListEvents
|
||||
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
|
||||
object DismissRequestVerificationPrompt : RoomListEvents
|
||||
object ToggleSearchResults : RoomListEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ class RoomListPresenter @Inject constructor(
|
|||
|
||||
Timber.v("RoomSummaries size = ${roomSummaries.size}")
|
||||
|
||||
val mappedRoomSummaries: MutableState<ImmutableList<RoomListRoomSummary>> = remember { mutableStateOf(persistentListOf()) }
|
||||
val filteredRoomSummaries: MutableState<ImmutableList<RoomListRoomSummary>> = remember {
|
||||
mutableStateOf(persistentListOf())
|
||||
}
|
||||
|
|
@ -101,41 +102,51 @@ class RoomListPresenter @Inject constructor(
|
|||
derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified && !verificationPromptDismissed }
|
||||
}
|
||||
|
||||
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
fun handleEvents(event: RoomListEvents) {
|
||||
when (event) {
|
||||
is RoomListEvents.UpdateFilter -> filter = event.newFilter
|
||||
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
|
||||
RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true
|
||||
RoomListEvents.ToggleSearchResults -> {
|
||||
if (displaySearchResults) {
|
||||
filter = ""
|
||||
}
|
||||
displaySearchResults =! displaySearchResults
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(roomSummaries, filter) {
|
||||
filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter)
|
||||
mappedRoomSummaries.value = if (roomSummaries.isEmpty()) {
|
||||
RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList()
|
||||
} else {
|
||||
mapRoomSummaries(roomSummaries).toImmutableList()
|
||||
}
|
||||
filteredRoomSummaries.value = updateFilteredRoomSummaries(mappedRoomSummaries.value, filter)
|
||||
}
|
||||
|
||||
val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
|
||||
|
||||
return RoomListState(
|
||||
matrixUser = matrixUser.value,
|
||||
roomList = filteredRoomSummaries.value,
|
||||
roomList = mappedRoomSummaries.value,
|
||||
filter = filter,
|
||||
filteredRoomList = filteredRoomSummaries.value,
|
||||
displayVerificationPrompt = displayVerificationPrompt,
|
||||
snackbarMessage = snackbarMessage,
|
||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||
displayInvites = invites.isNotEmpty(),
|
||||
displaySearchResults = displaySearchResults,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun updateFilteredRoomSummaries(roomSummaries: List<RoomSummary>?, filter: String): ImmutableList<RoomListRoomSummary> {
|
||||
if (roomSummaries.isNullOrEmpty()) {
|
||||
return RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList()
|
||||
}
|
||||
val mappedRoomSummaries = mapRoomSummaries(roomSummaries)
|
||||
return if (filter.isEmpty()) {
|
||||
mappedRoomSummaries
|
||||
} else {
|
||||
mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) }
|
||||
private fun updateFilteredRoomSummaries(mappedRoomSummaries: ImmutableList<RoomListRoomSummary>, filter: String): ImmutableList<RoomListRoomSummary> {
|
||||
return when {
|
||||
filter.isEmpty() -> emptyList()
|
||||
else -> mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) }
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,10 +26,12 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
data class RoomListState(
|
||||
val matrixUser: MatrixUser?,
|
||||
val roomList: ImmutableList<RoomListRoomSummary>,
|
||||
val filter: String,
|
||||
val filter: String?,
|
||||
val filteredRoomList: ImmutableList<RoomListRoomSummary>,
|
||||
val displayVerificationPrompt: Boolean,
|
||||
val hasNetworkConnection: Boolean,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val displayInvites: Boolean,
|
||||
val displaySearchResults: Boolean,
|
||||
val eventSink: (RoomListEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
|||
aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)),
|
||||
aRoomListState().copy(hasNetworkConnection = false),
|
||||
aRoomListState().copy(displayInvites = true),
|
||||
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
|
||||
aRoomListState().copy(displaySearchResults = true),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -43,10 +45,12 @@ internal fun aRoomListState() = RoomListState(
|
|||
matrixUser = MatrixUser(id = UserId("@id:domain"), username = "User#1", avatarData = AvatarData("@id:domain", "U")),
|
||||
roomList = aRoomListRoomSummaryList(),
|
||||
filter = "filter",
|
||||
filteredRoomList = aRoomListRoomSummaryList(),
|
||||
hasNetworkConnection = true,
|
||||
snackbarMessage = null,
|
||||
displayVerificationPrompt = false,
|
||||
displayInvites = false,
|
||||
displaySearchResults = false,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorVi
|
|||
import io.element.android.features.roomlist.impl.components.RoomListTopBar
|
||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchResultContent
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchResultView
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
|
@ -91,15 +93,27 @@ fun RoomListView(
|
|||
onCreateRoomClicked: () -> Unit = {},
|
||||
onInvitesClicked: () -> Unit = {},
|
||||
) {
|
||||
RoomListContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onCreateRoomClicked = onCreateRoomClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
)
|
||||
Column(modifier = modifier) {
|
||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||
Box {
|
||||
RoomListContent(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onCreateRoomClicked = onCreateRoomClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
)
|
||||
// This overlaid view will only be visible when state.displaySearchResults is true
|
||||
RoomListSearchResultView(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
|
|
@ -163,16 +177,14 @@ fun RoomListContent(
|
|||
Scaffold(
|
||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
Column {
|
||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||
RoomListTopBar(
|
||||
matrixUser = state.matrixUser,
|
||||
filter = state.filter,
|
||||
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
||||
onOpenSettings = onOpenSettings,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
RoomListTopBar(
|
||||
matrixUser = state.matrixUser,
|
||||
areSearchResultsDisplayed = state.displaySearchResults,
|
||||
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
||||
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
|
||||
onOpenSettings = onOpenSettings,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
content = { padding ->
|
||||
Column(
|
||||
|
|
@ -306,7 +318,7 @@ internal fun PreviewRequestVerificationHeaderDark() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun RoomListRoomSummary.contentType() = isPlaceholder
|
||||
internal fun RoomListRoomSummary.contentType() = isPlaceholder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
|
@ -322,3 +334,11 @@ internal fun RoomListViewDarkPreview(@PreviewParameter(RoomListStateProvider::cl
|
|||
private fun ContentToPreview(state: RoomListState) {
|
||||
RoomListView(state)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListSearchResultContentPreview() {
|
||||
ElementPreviewLight {
|
||||
RoomListSearchResultContent(state = aRoomListState(), onRoomClicked = {})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,57 +20,42 @@ package io.element.android.features.roomlist.impl.components
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.roomlist.impl.R
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
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.MediumTopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomListTopBar(
|
||||
matrixUser: MatrixUser?,
|
||||
filter: String,
|
||||
areSearchResultsDisplayed: Boolean,
|
||||
onFilterChanged: (String) -> Unit,
|
||||
onToggleSearch: () -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -79,124 +64,26 @@ fun RoomListTopBar(
|
|||
tag = "RoomListScreen",
|
||||
msg = "TopBar"
|
||||
)
|
||||
var searchWidgetStateIsOpened by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
fun closeFilter() {
|
||||
onFilterChanged("")
|
||||
searchWidgetStateIsOpened = false
|
||||
}
|
||||
|
||||
BackHandler(enabled = searchWidgetStateIsOpened) {
|
||||
BackHandler(enabled = areSearchResultsDisplayed) {
|
||||
closeFilter()
|
||||
onToggleSearch()
|
||||
}
|
||||
|
||||
if (searchWidgetStateIsOpened) {
|
||||
SearchRoomListTopBar(
|
||||
text = filter,
|
||||
onFilterChanged = onFilterChanged,
|
||||
onCloseClicked = ::closeFilter,
|
||||
scrollBehavior = scrollBehavior,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
DefaultRoomListTopBar(
|
||||
matrixUser = matrixUser,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onSearchClicked = {
|
||||
searchWidgetStateIsOpened = true
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchRoomListTopBar(
|
||||
text: String,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
modifier: Modifier = Modifier,
|
||||
onFilterChanged: (String) -> Unit = {},
|
||||
onCloseClicked: () -> Unit = {},
|
||||
) {
|
||||
var filterState by textFieldState(stateValue = text)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
TopAppBar(
|
||||
modifier = modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
title = {
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
value = filterState,
|
||||
textStyle = TextStyle(
|
||||
fontSize = 17.sp
|
||||
),
|
||||
onValueChange = {
|
||||
filterState = it
|
||||
onFilterChanged(it)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(id = StringR.string.action_search),
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = ContentAlpha.medium)
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (text.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onFilterChanged("")
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "clear",
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onCloseClicked()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "close",
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets(0.dp)
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SearchRoomListTopBarLightPreview() = ElementPreviewLight { SearchRoomListTopBarPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SearchRoomListTopBarDarkPreview() = ElementPreviewDark { SearchRoomListTopBarPreview() }
|
||||
|
||||
@Composable
|
||||
private fun SearchRoomListTopBarPreview() {
|
||||
SearchRoomListTopBar(
|
||||
text = "Hello",
|
||||
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
|
||||
DefaultRoomListTopBar(
|
||||
matrixUser = matrixUser,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onSearchClicked = onToggleSearch,
|
||||
scrollBehavior = scrollBehavior,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun DefaultRoomListTopBar(
|
||||
matrixUser: MatrixUser?,
|
||||
|
|
@ -216,21 +103,19 @@ private fun DefaultRoomListTopBar(
|
|||
},
|
||||
navigationIcon = {
|
||||
if (matrixUser != null) {
|
||||
IconButton(onClick = {}) {
|
||||
Avatar(matrixUser.avatarData)
|
||||
IconButton(
|
||||
modifier = Modifier.testTag(TestTags.homeScreenSettings),
|
||||
onClick = onOpenSettings
|
||||
) {
|
||||
Avatar(matrixUser.avatarData, contentDescription = stringResource(StringR.string.common_settings))
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = onSearchClicked
|
||||
onClick = onSearchClicked,
|
||||
) {
|
||||
Icon(Icons.Default.Search, contentDescription = "search")
|
||||
}
|
||||
IconButton(
|
||||
onClick = onOpenSettings
|
||||
) {
|
||||
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
||||
Icon(Icons.Default.Search, contentDescription = stringResource(StringR.string.action_search))
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
|
|
@ -246,6 +131,7 @@ internal fun DefaultRoomListTopBarLightPreview() = ElementPreviewLight { Default
|
|||
@Composable
|
||||
internal fun DefaultRoomListTopBarDarkPreview() = ElementPreviewDark { DefaultRoomListTopBarPreview() }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun DefaultRoomListTopBarPreview() {
|
||||
DefaultRoomListTopBar(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl.search
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
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.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomlist.impl.RoomListEvents
|
||||
import io.element.android.features.roomlist.impl.RoomListState
|
||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||
import io.element.android.features.roomlist.impl.contentType
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.modifiers.applyIf
|
||||
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.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.copy
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
|
||||
@Composable
|
||||
internal fun RoomListSearchResultView(
|
||||
state: RoomListState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = state.displaySearchResults,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.applyIf(state.displaySearchResults, ifTrue = {
|
||||
// Disable input interaction to underlying views
|
||||
pointerInput(Unit) {}
|
||||
})
|
||||
) {
|
||||
if (state.displaySearchResults) {
|
||||
RoomListSearchResultContent(state = state, onRoomClicked = onRoomClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun RoomListSearchResultContent(
|
||||
state: RoomListState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val borderColor = MaterialTheme.colorScheme.tertiary
|
||||
val strokeWidth = 1.dp
|
||||
fun onBackButtonPressed() {
|
||||
state.eventSink(RoomListEvents.ToggleSearchResults)
|
||||
}
|
||||
fun onRoomClicked(room: RoomListRoomSummary) {
|
||||
if (room.roomId == null) return
|
||||
onRoomClicked(room.roomId)
|
||||
}
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier.drawBehind {
|
||||
drawLine(
|
||||
color = borderColor,
|
||||
start = Offset(0f, size.height),
|
||||
end = Offset(size.width, size.height),
|
||||
strokeWidth = strokeWidth.value
|
||||
)
|
||||
},
|
||||
navigationIcon = { BackButton(onClick = ::onBackButtonPressed) },
|
||||
title = {
|
||||
val filter = state.filter.orEmpty()
|
||||
val focusRequester = FocusRequester()
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
value = filter,
|
||||
onValueChange = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
containerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
errorIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent
|
||||
),
|
||||
trailingIcon = {
|
||||
if (filter.isNotEmpty()) {
|
||||
IconButton(onClick = {
|
||||
state.eventSink(RoomListEvents.UpdateFilter(""))
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.action_cancel)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(state.displaySearchResults) {
|
||||
if (state.displaySearchResults) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
},
|
||||
windowInsets = TopAppBarDefaults.windowInsets.copy(top = 0)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
val lazyListState = rememberLazyListState()
|
||||
val visibleRange by remember {
|
||||
derivedStateOf {
|
||||
val layoutInfo = lazyListState.layoutInfo
|
||||
val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0
|
||||
val size = layoutInfo.visibleItemsInfo.size
|
||||
firstItemIndex until firstItemIndex + size
|
||||
}
|
||||
}
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override suspend fun onPostFling(
|
||||
consumed: Velocity,
|
||||
available: Velocity
|
||||
): Velocity {
|
||||
state.eventSink(RoomListEvents.UpdateVisibleRange(visibleRange))
|
||||
return super.onPostFling(consumed, available)
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
state = lazyListState,
|
||||
) {
|
||||
items(
|
||||
items = state.filteredRoomList,
|
||||
contentType = { room -> room.contentType() },
|
||||
) { room ->
|
||||
RoomSummaryRow(room = room, onClick = ::onRoomClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_roomlist_main_space_title">"Alle Chats"</string>
|
||||
<string name="state_event_avatar_changed_too">"(Avatar wurde ebenfalls geändert)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s hat seinen Avatar geändert"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Du hast deinen Avatar geändert"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s hat den Anzeigenamen von %2$s in %3$s geändert"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Du hast deinen Anzeigenamen von %1$s in %2$s geändert"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s hat den Anzeigenamen entfernt (war %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Du hast deinen Anzeigenamen entfernt (war %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s hat den Anzeigenamen auf %2$s gesetzt"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Du hast deinen Anzeigenamen auf %1$s gesetzt"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s hat den Raum-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Du hast den Raum-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s hat den Raum-Avatar entfernt"</string>
|
||||
<string name="state_event_room_created">"%1$s hat den Raum erstellt"</string>
|
||||
<string name="state_event_room_created_by_you">"Du hast den Raum erstellt"</string>
|
||||
<string name="state_event_room_invite">"%1$s hat %2$s eingeladen"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s hat die Einladung angenommen"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Du hast die Einladung angenommen"</string>
|
||||
<string name="state_event_room_invite_by_you">"Du hast %1$s eingeladen"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s hat dich eingeladen"</string>
|
||||
<string name="state_event_room_join">"%1$s ist dem Raum beigetreten"</string>
|
||||
<string name="state_event_room_join_by_you">"Du bist dem Raum beigetreten"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s hat deine Beitrittsanfrage abgelehnt"</string>
|
||||
<string name="state_event_room_leave">"%1$s hat den Raum verlassen"</string>
|
||||
<string name="state_event_room_leave_by_you">"Du hast den Raum verlassen"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s hat den Raumnamen geändert in: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Sie haben den Raumnamen geändert in: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s hat den Raumnamen entfernt"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Du hast den Raumnamen entfernt"</string>
|
||||
<string name="state_event_room_reject">"%1$s hat die Einladung abgelehnt"</string>
|
||||
<string name="state_event_room_reject_by_you">"Du hast die Einladung abgelehnt"</string>
|
||||
<string name="state_event_room_remove">"%1$s hat %2$s entfernt"</string>
|
||||
<string name="state_event_room_remove_by_you">"Du hast %1$s entfernt"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s hat das Thema geändert zu: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Sie haben das Thema geändert zu: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s hat das Raumthema entfernt"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Du hast das Raumthema entfernt"</string>
|
||||
</resources>
|
||||
|
|
@ -112,6 +112,8 @@ class RoomListPresenterTests {
|
|||
withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t"))
|
||||
val withFilterState = awaitItem()
|
||||
Truth.assertThat(withFilterState.filter).isEqualTo("t")
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,17 +170,18 @@ class RoomListPresenterTests {
|
|||
val loadedState = awaitItem()
|
||||
// Test filtering with result
|
||||
loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3)))
|
||||
skipItems(1) // Filter update
|
||||
val withNotFilteredRoomState = awaitItem()
|
||||
Truth.assertThat(withNotFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
|
||||
Truth.assertThat(withNotFilteredRoomState.roomList.size).isEqualTo(1)
|
||||
Truth.assertThat(withNotFilteredRoomState.roomList.first())
|
||||
Truth.assertThat(withNotFilteredRoomState.filteredRoomList.size).isEqualTo(1)
|
||||
Truth.assertThat(withNotFilteredRoomState.filteredRoomList.first())
|
||||
.isEqualTo(aRoomListRoomSummary)
|
||||
// Test filtering without result
|
||||
withNotFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
|
||||
skipItems(1) // Filter update
|
||||
val withFilteredRoomState = awaitItem()
|
||||
Truth.assertThat(withFilteredRoomState.filter).isEqualTo("tada")
|
||||
Truth.assertThat(withFilteredRoomState.roomList).isEmpty()
|
||||
Truth.assertThat(withFilteredRoomState.filteredRoomList).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_session_verification_compare_emojis_title">"Emojis vergleichen"</string>
|
||||
<string name="screen_session_verification_open_existing_session_subtitle">"Beweise, dass du es bist, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen."</string>
|
||||
<string name="screen_session_verification_positive_button_initial">"Ich bin bereit"</string>
|
||||
<string name="screen_session_verification_positive_button_verifying_ongoing">"Warten auf Übereinstimmung"</string>
|
||||
<string name="screen_session_verification_they_dont_match">"Sie stimmen nicht überein"</string>
|
||||
<string name="screen_session_verification_they_match">"Sie stimmen überein"</string>
|
||||
<string name="screen_session_verification_cancelled_title">"Verifizierung abgebrochen"</string>
|
||||
</resources>
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_session_verification_cancelled_subtitle">"Algo no fue bien. Se agotó el tiempo de espera de la solicitud o se rechazó."</string>
|
||||
<string name="screen_session_verification_cancelled_title">"Verificación cancelada"</string>
|
||||
<string name="screen_session_verification_compare_emojis_subtitle">"Confirma que los emojis que aparecen a continuación coinciden con los que aparecen en tu otra sesión."</string>
|
||||
<string name="screen_session_verification_compare_emojis_title">"Comparar emojis"</string>
|
||||
<string name="screen_session_verification_complete_subtitle">"Tu nueva sesión ya está verificada. Tienes acceso a tus mensajes cifrados y otros usuarios lo considerarán de confianza."</string>
|
||||
|
|
@ -9,11 +8,12 @@
|
|||
<string name="screen_session_verification_open_existing_session_title">"Abrir una sesión existente"</string>
|
||||
<string name="screen_session_verification_positive_button_canceled">"Reintentar la verificación"</string>
|
||||
<string name="screen_session_verification_positive_button_initial">"Estoy listo"</string>
|
||||
<string name="screen_session_verification_positive_button_ready">"Comenzar"</string>
|
||||
<string name="screen_session_verification_positive_button_verifying_ongoing">"Esperando a que coincida"</string>
|
||||
<string name="screen_session_verification_request_accepted_subtitle">"Compara los emoji, asegurándote de que aparecen en el mismo orden."</string>
|
||||
<string name="screen_session_verification_they_dont_match">"No coinciden"</string>
|
||||
<string name="screen_session_verification_they_match">"Coinciden"</string>
|
||||
<string name="screen_session_verification_waiting_to_accept_subtitle">"Acepta la solicitud para iniciar el proceso de verificación en tu otra sesión para continuar."</string>
|
||||
<string name="screen_session_verification_waiting_to_accept_title">"A la espera de aceptar la solicitud"</string>
|
||||
<string name="screen_session_verification_cancelled_title">"Verificación cancelada"</string>
|
||||
<string name="screen_session_verification_positive_button_ready">"Comenzar"</string>
|
||||
</resources>
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_session_verification_cancelled_subtitle">"C\'è qualcosa che non va. La richiesta è scaduta o è stata rifiutata."</string>
|
||||
<string name="screen_session_verification_cancelled_title">"Verifica annullata"</string>
|
||||
<string name="screen_session_verification_compare_emojis_subtitle">"Verifica che gli emoji sottostanti corrispondano a quelli mostrati nell\'altra sessione."</string>
|
||||
<string name="screen_session_verification_compare_emojis_title">"Confronta le emoji"</string>
|
||||
<string name="screen_session_verification_complete_subtitle">"La tua nuova sessione è ora verificata. Ha accesso ai tuoi messaggi crittografati e gli altri utenti la vedranno come attendibile."</string>
|
||||
|
|
@ -9,11 +8,12 @@
|
|||
<string name="screen_session_verification_open_existing_session_title">"Apri una sessione esistente"</string>
|
||||
<string name="screen_session_verification_positive_button_canceled">"Riprova la verifica"</string>
|
||||
<string name="screen_session_verification_positive_button_initial">"Sono pronto"</string>
|
||||
<string name="screen_session_verification_positive_button_ready">"Inizia"</string>
|
||||
<string name="screen_session_verification_positive_button_verifying_ongoing">"In attesa di un riscontro"</string>
|
||||
<string name="screen_session_verification_request_accepted_subtitle">"Confronta le emoji uniche, assicurandoti che appaiano nello stesso ordine."</string>
|
||||
<string name="screen_session_verification_they_dont_match">"Non corrispondono"</string>
|
||||
<string name="screen_session_verification_they_match">"Corrispondono"</string>
|
||||
<string name="screen_session_verification_waiting_to_accept_subtitle">"Accetta la richiesta di avviare il processo di verifica nell\'altra sessione per continuare."</string>
|
||||
<string name="screen_session_verification_waiting_to_accept_title">"In attesa di accettare la richiesta"</string>
|
||||
<string name="screen_session_verification_cancelled_title">"Verifica annullata"</string>
|
||||
<string name="screen_session_verification_positive_button_ready">"Inizia"</string>
|
||||
</resources>
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_session_verification_cancelled_subtitle">"Ceva nu este în regulă. Fie cererea a expirat, fie a fost respinsă."</string>
|
||||
<string name="screen_session_verification_cancelled_title">"Verificare anulată"</string>
|
||||
<string name="screen_session_verification_compare_emojis_subtitle">"Confirmați că emoticoanele de mai jos se potrivesc cu cele afișate în cealaltă sesiune."</string>
|
||||
<string name="screen_session_verification_compare_emojis_title">"Comparați emoticoanele"</string>
|
||||
<string name="screen_session_verification_complete_subtitle">"Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar alți utilizatori vă vor vedea ca fiind de încredere."</string>
|
||||
|
|
@ -9,11 +8,12 @@
|
|||
<string name="screen_session_verification_open_existing_session_title">"Deschideți o sesiune existentă"</string>
|
||||
<string name="screen_session_verification_positive_button_canceled">"Reîncercați verificarea"</string>
|
||||
<string name="screen_session_verification_positive_button_initial">"Sunt pregătit"</string>
|
||||
<string name="screen_session_verification_positive_button_ready">"Începeți"</string>
|
||||
<string name="screen_session_verification_positive_button_verifying_ongoing">"Se așteaptă confirmarea"</string>
|
||||
<string name="screen_session_verification_request_accepted_subtitle">"Comparăți emoticoalene asigurându-vă că apar în aceeași ordine."</string>
|
||||
<string name="screen_session_verification_they_dont_match">"Nu se potrivesc"</string>
|
||||
<string name="screen_session_verification_they_match">"Se potrivesc"</string>
|
||||
<string name="screen_session_verification_waiting_to_accept_subtitle">"Acceptați solicitarea de a începe procesul de verificare în cealaltă sesiune pentru a continua."</string>
|
||||
<string name="screen_session_verification_waiting_to_accept_title">"Se așteptă acceptarea cererii"</string>
|
||||
<string name="screen_session_verification_cancelled_title">"Verificare anulată"</string>
|
||||
<string name="screen_session_verification_positive_button_ready">"Începeți"</string>
|
||||
</resources>
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_session_verification_cancelled_subtitle">"Something doesn’t seem right. Either the request timed out or the request was denied."</string>
|
||||
<string name="screen_session_verification_cancelled_title">"Verification cancelled"</string>
|
||||
<string name="screen_session_verification_compare_emojis_subtitle">"Confirm that the emojis below match those shown on your other session."</string>
|
||||
<string name="screen_session_verification_compare_emojis_title">"Compare emojis"</string>
|
||||
<string name="screen_session_verification_complete_subtitle">"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted."</string>
|
||||
|
|
@ -9,11 +8,12 @@
|
|||
<string name="screen_session_verification_open_existing_session_title">"Open an existing session"</string>
|
||||
<string name="screen_session_verification_positive_button_canceled">"Retry verification"</string>
|
||||
<string name="screen_session_verification_positive_button_initial">"I am ready"</string>
|
||||
<string name="screen_session_verification_positive_button_ready">"Start"</string>
|
||||
<string name="screen_session_verification_positive_button_verifying_ongoing">"Waiting to match"</string>
|
||||
<string name="screen_session_verification_request_accepted_subtitle">"Compare the unique emoji, ensuring they appear in the same order."</string>
|
||||
<string name="screen_session_verification_they_dont_match">"They don’t match"</string>
|
||||
<string name="screen_session_verification_they_match">"They match"</string>
|
||||
<string name="screen_session_verification_waiting_to_accept_subtitle">"Accept the request to start the verification process in your other session to continue."</string>
|
||||
<string name="screen_session_verification_waiting_to_accept_title">"Waiting to accept request"</string>
|
||||
<string name="screen_session_verification_cancelled_title">"Verification cancelled"</string>
|
||||
<string name="screen_session_verification_positive_button_ready">"Start"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
/** Create a Flow emitting a single error event. It should be useful for tests. */
|
||||
fun <T> errorFlow(throwable: Throwable) = flow<T> { throw throwable }
|
||||
|
|
@ -41,7 +41,11 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
|
||||
fun Avatar(
|
||||
avatarData: AvatarData,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
val commonModifier = modifier
|
||||
.size(avatarData.size.dp)
|
||||
.clip(CircleShape)
|
||||
|
|
@ -54,6 +58,7 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
|
|||
ImageAvatar(
|
||||
avatarData = avatarData,
|
||||
modifier = commonModifier,
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -62,13 +67,14 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
|
|||
private fun ImageAvatar(
|
||||
avatarData: AvatarData,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = avatarData,
|
||||
onError = {
|
||||
Timber.e("TAG", "Error $it\n${it.result}", it.result.throwable)
|
||||
},
|
||||
contentDescription = null,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = ContentScale.Crop,
|
||||
placeholder = debugPlaceholderAvatar(),
|
||||
modifier = modifier
|
||||
|
|
@ -89,7 +95,7 @@ private fun InitialsAvatar(
|
|||
end = Offset(100f, 0f)
|
||||
)
|
||||
Box(
|
||||
modifier.background(brush = initialsGradient)
|
||||
modifier.background(brush = initialsGradient),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.modifiers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
|
||||
/**
|
||||
* Applies the [ifTrue] modifier when the [condition] is true, [ifFalse] otherwise.
|
||||
*/
|
||||
@SuppressLint("UnnecessaryComposedModifier") // It's actually necessary due to the `@Composable` lambdas
|
||||
fun Modifier.applyIf(
|
||||
condition: Boolean,
|
||||
ifTrue: @Composable Modifier.() -> Modifier,
|
||||
ifFalse: @Composable (Modifier.() -> Modifier)? = null
|
||||
): Modifier =
|
||||
composed(
|
||||
inspectorInfo = debugInspectorInfo {
|
||||
name = "applyIf"
|
||||
value = condition
|
||||
}
|
||||
) {
|
||||
when {
|
||||
condition -> then(ifTrue(Modifier))
|
||||
ifFalse != null -> then(ifFalse(Modifier))
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.modifiers
|
||||
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import kotlin.math.sqrt
|
||||
|
||||
// Note: these modifiers come from https://gist.github.com/darvld/eb3844474baf2f3fc6d3ab44a4b4b5f8
|
||||
|
||||
/**
|
||||
* A modifier that clips the composable content using an animated circle. The circle will
|
||||
* expand/shrink with an animation whenever [visible] changes.
|
||||
*
|
||||
* For more fine-grained control over the transition, see this method's overload, which allows passing
|
||||
* a [State] object to control the progress of the reveal animation.
|
||||
*
|
||||
* By default, the circle is centered in the content, but custom positions may be specified using
|
||||
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).*/
|
||||
fun Modifier.circularReveal(
|
||||
visible: Boolean,
|
||||
showScrim: Boolean = false,
|
||||
revealFrom: Offset = Offset(0.5f, 0.5f),
|
||||
): Modifier = composed(
|
||||
factory = {
|
||||
val factor = updateTransition(visible, label = "Visibility")
|
||||
.animateFloat(label = "revealFactor") { if (it) 1f else 0f }
|
||||
|
||||
circularReveal(factor, showScrim, revealFrom)
|
||||
},
|
||||
inspectorInfo = debugInspectorInfo {
|
||||
name = "circularReveal"
|
||||
properties["visible"] = visible
|
||||
properties["revealFrom"] = revealFrom
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* A modifier that clips the composable content using a circular shape. The radius of the circle
|
||||
* will be determined by the [transitionProgress].
|
||||
*
|
||||
* The values of the progress should be between 0 and 1.
|
||||
*
|
||||
* By default, the circle is centered in the content, but custom positions may be specified using
|
||||
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).
|
||||
* */
|
||||
fun Modifier.circularReveal(
|
||||
transitionProgress: State<Float>,
|
||||
showScrim: Boolean = false,
|
||||
revealFrom: Offset = Offset(0.5f, 0.5f)
|
||||
): Modifier {
|
||||
return drawWithCache {
|
||||
val path = Path()
|
||||
val center = revealFrom.mapTo(size)
|
||||
val radius = calculateRadius(revealFrom, size)
|
||||
val scrimColor = if (showScrim)
|
||||
Color.Gray
|
||||
else
|
||||
Color.Transparent
|
||||
|
||||
path.addOval(Rect(center, radius * transitionProgress.value))
|
||||
|
||||
onDrawWithContent {
|
||||
if (showScrim) {
|
||||
drawRect(scrimColor, alpha = transitionProgress.value * 0.75f)
|
||||
}
|
||||
clipPath(path) { this@onDrawWithContent.drawContent() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Offset.mapTo(size: Size): Offset {
|
||||
return Offset(x * size.width, y * size.height)
|
||||
}
|
||||
|
||||
private fun calculateRadius(normalizedOrigin: Offset, size: Size) = with(normalizedOrigin) {
|
||||
val x = (if (x > 0.5f) x else 1 - x) * size.width
|
||||
val y = (if (y > 0.5f) y else 1 - y) * size.height
|
||||
|
||||
sqrt(x * x + y * y)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.utils
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
|
||||
@Composable
|
||||
fun WindowInsets.copy(
|
||||
top: Int? = null,
|
||||
right: Int? = null,
|
||||
bottom: Int? = null,
|
||||
left: Int? = null
|
||||
): WindowInsets {
|
||||
val density = LocalDensity.current
|
||||
val direction = LocalLayoutDirection.current
|
||||
return WindowInsets(
|
||||
top = top ?: this.getTop(density),
|
||||
right = right ?: this.getRight(density, direction),
|
||||
bottom = bottom ?: this.getBottom(density),
|
||||
left = left ?: this.getLeft(density, direction)
|
||||
)
|
||||
}
|
||||
|
|
@ -68,6 +68,10 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
||||
|
||||
suspend fun ignoreUser(userId: UserId): Result<Unit>
|
||||
|
||||
suspend fun unignoreUser(userId: UserId): Result<Unit>
|
||||
|
||||
suspend fun leave(): Result<Unit>
|
||||
|
||||
suspend fun acceptInvitation(): Result<Unit>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncRoom
|
|||
import org.matrix.rustcomponents.sdk.UpdateSummary
|
||||
import org.matrix.rustcomponents.sdk.genTransactionId
|
||||
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
||||
import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
|
||||
|
||||
class RustMatrixRoom(
|
||||
override val sessionId: SessionId,
|
||||
|
|
@ -53,6 +54,16 @@ class RustMatrixRoom(
|
|||
|
||||
private var _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
|
||||
|
||||
private val timeline by lazy {
|
||||
RustMatrixTimeline(
|
||||
matrixRoom = this,
|
||||
innerRoom = innerRoom,
|
||||
slidingSyncRoom = slidingSyncRoom,
|
||||
coroutineScope = coroutineScope,
|
||||
coroutineDispatchers = coroutineDispatchers
|
||||
)
|
||||
}
|
||||
|
||||
override fun syncUpdateFlow(): Flow<Long> {
|
||||
return slidingSyncUpdateFlow
|
||||
.filter {
|
||||
|
|
@ -65,13 +76,7 @@ class RustMatrixRoom(
|
|||
}
|
||||
|
||||
override fun timeline(): MatrixTimeline {
|
||||
return RustMatrixTimeline(
|
||||
matrixRoom = this,
|
||||
innerRoom = innerRoom,
|
||||
slidingSyncRoom = slidingSyncRoom,
|
||||
coroutineScope = coroutineScope,
|
||||
coroutineDispatchers = coroutineDispatchers
|
||||
)
|
||||
return timeline
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
|
@ -125,11 +130,11 @@ class RustMatrixRoom(
|
|||
_membersStateFlow.value = MatrixRoomMembersState.Pending
|
||||
runCatching {
|
||||
innerRoom.members().map(RoomMemberMapper::map)
|
||||
}.onSuccess {
|
||||
}.map {
|
||||
_membersStateFlow.value = MatrixRoomMembersState.Ready(it)
|
||||
}.onFailure {
|
||||
_membersStateFlow.value = MatrixRoomMembersState.Error(it)
|
||||
}.map { }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun userDisplayName(userId: UserId): Result<String?> =
|
||||
|
|
@ -195,4 +200,19 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun ignoreUser(userId: UserId): Result<Unit> {
|
||||
return runCatching {
|
||||
getRustMember(userId)?.ignore() ?: error("No member with userId $userId exists in room $roomId")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unignoreUser(userId: UserId): Result<Unit> {
|
||||
return runCatching {
|
||||
getRustMember(userId)?.unignore() ?: error("No member with userId $userId exists in room $roomId")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRustMember(userId: UserId): RustRoomMember? {
|
||||
return innerRoom.members().find { it.userId() == userId.value }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,9 +151,9 @@ class RustMatrixTimeline(
|
|||
requiredState = listOf(
|
||||
RequiredState(key = "m.room.canonical_alias", value = ""),
|
||||
RequiredState(key = "m.room.topic", value = ""),
|
||||
RequiredState(key = "m.room.name", value = ""),
|
||||
RequiredState(key = "m.room.join_rule", value = ""),
|
||||
RequiredState(key = "m.room.join_rules", value = ""),
|
||||
),
|
||||
//TODO allow configuration
|
||||
timelineLimit = 20.toUInt()
|
||||
)
|
||||
val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.core)
|
||||
api(projects.libraries.matrix.api)
|
||||
api(libs.coroutines.core)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
|
|
@ -48,6 +47,8 @@ class FakeMatrixRoom(
|
|||
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
|
||||
) : MatrixRoom {
|
||||
|
||||
private var ignoreResult: Result<Unit> = Result.success(Unit)
|
||||
private var unignoreResult: Result<Unit> = Result.success(Unit)
|
||||
private var userDisplayNameResult = Result.success<String?>(null)
|
||||
private var userAvatarUrlResult = Result.success<String?>(null)
|
||||
private var updateMembersResult: Result<Unit> = Result.success(Unit)
|
||||
|
|
@ -116,6 +117,10 @@ class FakeMatrixRoom(
|
|||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun ignoreUser(userId: UserId): Result<Unit> = ignoreResult
|
||||
|
||||
override suspend fun unignoreUser(userId: UserId): Result<Unit> = unignoreResult
|
||||
|
||||
override suspend fun leave(): Result<Unit> = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit)
|
||||
override suspend fun acceptInvitation(): Result<Unit> {
|
||||
isInviteAccepted = true
|
||||
|
|
@ -157,4 +162,11 @@ class FakeMatrixRoom(
|
|||
rejectInviteResult = result
|
||||
}
|
||||
|
||||
fun givenIgnoreResult(result: Result<Unit>) {
|
||||
ignoreResult = result
|
||||
}
|
||||
|
||||
fun givenUnIgnoreResult(result: Result<Unit>) {
|
||||
unignoreResult = result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): Notif
|
|||
isRemote = false,
|
||||
localSendState = null,
|
||||
reactions = emptyList(),
|
||||
sender = UserId(""),
|
||||
sender = UserId("@user:domain"),
|
||||
senderProfile = ProfileTimelineDetails.Unavailable,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
content = MessageContent(
|
||||
|
|
|
|||
30
libraries/push/impl/src/main/res/values-de/translations.xml
Normal file
30
libraries/push/impl/src/main/res/values-de/translations.xml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="notification_channel_noisy">"Laute Benachrichtigungen"</string>
|
||||
<string name="notification_invitation_action_join">"Beitreten"</string>
|
||||
<string name="notification_invitation_action_reject">"Ablehnen"</string>
|
||||
<string name="notification_new_messages">"Neue Nachrichten"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Als gelesen markieren"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s in %2$s und %3$s"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d Nachricht"</item>
|
||||
<item quantity="other">"%1$s: %2$d Nachrichten"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="one">"%d Einladung"</item>
|
||||
<item quantity="other">"%d Einladungen"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
<item quantity="one">"%d neue Nachricht"</item>
|
||||
<item quantity="other">"%d neue Nachrichten"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="one">"%d Raum"</item>
|
||||
<item quantity="other">"%d Räume"</item>
|
||||
</plurals>
|
||||
<string name="push_distributor_firebase_android">"Google-Dienste"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig."</string>
|
||||
<string name="notification_room_action_quick_reply">"Schnellantwort"</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="notification_room_action_quick_reply">"Respuesta rápida"</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="notification_room_action_quick_reply">"Risposta rapida"</string>
|
||||
</resources>
|
||||
54
libraries/push/impl/src/main/res/values-ro/translations.xml
Normal file
54
libraries/push/impl/src/main/res/values-ro/translations.xml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="notification_channel_call">"Apel"</string>
|
||||
<string name="notification_channel_listening_for_events">"Ascultare evenimente"</string>
|
||||
<string name="notification_channel_noisy">"Notificări zgomotoase"</string>
|
||||
<string name="notification_channel_silent">"Notificări silențioase"</string>
|
||||
<string name="notification_inline_reply_failed">"** Trimiterea eșuată - vă rugăm să deschideți camera"</string>
|
||||
<string name="notification_invitation_action_join">"Alăturați-vă"</string>
|
||||
<string name="notification_invitation_action_reject">"Respingeți"</string>
|
||||
<string name="notification_new_messages">"Mesaje noi"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Marcați ca citit"</string>
|
||||
<string name="notification_sender_me">"Eu"</string>
|
||||
<string name="notification_test_push_notification_content">"Vizualizați o notificare! Faceți clic pe mine!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s și %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room">"%1$s în %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s în %2$s și %3$s"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d mesaj"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%1$s: %2$d mesaje"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_compat_summary_title">
|
||||
<item quantity="one">"%d notificare"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%d notificări"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="one">"%d invitație"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%d invitații"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
<item quantity="one">"%d mesaj nou"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%d mesaje noi"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
<item quantity="one">"%d mesaj notificat necitit"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%d mesaje notificate necitite"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="one">"%d cameră"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%d camere"</item>
|
||||
</plurals>
|
||||
<string name="push_choose_distributor_dialog_title_android">"Alegeți modul de primire a notificărilor"</string>
|
||||
<string name="push_distributor_background_sync_android">"Sincronizare în fundal"</string>
|
||||
<string name="push_distributor_firebase_android">"Servicii Google"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Nu au fost găsite servicii Google Play valide. Este posibil ca notificările să nu funcționeze corect."</string>
|
||||
<string name="notification_room_action_quick_reply">"Raspuns rapid"</string>
|
||||
</resources>
|
||||
|
|
@ -9,7 +9,6 @@
|
|||
<string name="notification_invitation_action_reject">"Reject"</string>
|
||||
<string name="notification_new_messages">"New Messages"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Mark as read"</string>
|
||||
<string name="notification_room_action_quick_reply">"Quick reply"</string>
|
||||
<string name="notification_sender_me">"Me"</string>
|
||||
<string name="notification_test_push_notification_content">"You are viewing the notification! Click me!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
|
|
@ -45,4 +44,5 @@
|
|||
<string name="push_distributor_background_sync_android">"Background synchronization"</string>
|
||||
<string name="push_distributor_firebase_android">"Google Services"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"No valid Google Play Services found. Notifications may not work properly."</string>
|
||||
<string name="notification_room_action_quick_reply">"Quick reply"</string>
|
||||
</resources>
|
||||
|
|
@ -38,6 +38,11 @@ object TestTags {
|
|||
*/
|
||||
val changeServerServer = TestTag("change_server-server")
|
||||
val changeServerContinue = TestTag("change_server-continue")
|
||||
|
||||
/**
|
||||
* Room list / Home screen.
|
||||
*/
|
||||
val homeScreenSettings = TestTag("home_screen-settings")
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="rich_text_editor_composer_placeholder">"Nachricht…"</string>
|
||||
<string name="rich_text_editor_link">"Link setzen"</string>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,106 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_hide_password">"Passwort ausblenden"</string>
|
||||
<string name="a11y_send_files">"Dateien senden"</string>
|
||||
<string name="a11y_show_password">"Passwort anzeigen"</string>
|
||||
<string name="a11y_user_menu">"Benutzermenü"</string>
|
||||
<string name="action_back">"Zurück"</string>
|
||||
<string name="action_cancel">"Abbrechen"</string>
|
||||
<string name="action_choose_photo">"Foto auswählen"</string>
|
||||
<string name="action_close">"Schließen"</string>
|
||||
<string name="action_complete_verification">"Verifizierung abschließen"</string>
|
||||
<string name="action_confirm">"Bestätigen"</string>
|
||||
<string name="action_copy">"Kopieren"</string>
|
||||
<string name="action_copy_link">"Link kopieren"</string>
|
||||
<string name="action_create">"Erstellen"</string>
|
||||
<string name="action_decline">"Ablehnen"</string>
|
||||
<string name="action_disable">"Deaktivieren"</string>
|
||||
<string name="action_done">"Fertig"</string>
|
||||
<string name="action_edit">"Bearbeiten"</string>
|
||||
<string name="action_enable">"Aktivieren"</string>
|
||||
<string name="action_invite">"Einladen"</string>
|
||||
<string name="action_invite_friends_to_app">"Freunde zu %1$s einladen"</string>
|
||||
<string name="action_invites_list">"Einladungen"</string>
|
||||
<string name="action_learn_more">"Mehr erfahren"</string>
|
||||
<string name="action_leave">"Verlassen"</string>
|
||||
<string name="action_leave_room">"Raum verlassen"</string>
|
||||
<string name="action_next">"Weiter"</string>
|
||||
<string name="action_no">"Nein"</string>
|
||||
<string name="action_ok">"OK"</string>
|
||||
<string name="action_quick_reply">"Schnellantwort"</string>
|
||||
<string name="action_quote">"Zitieren"</string>
|
||||
<string name="action_remove">"Entfernen"</string>
|
||||
<string name="action_report_bug">"Fehler melden"</string>
|
||||
<string name="action_report_content">"Inhalt melden"</string>
|
||||
<string name="action_retry">"Erneut versuchen"</string>
|
||||
<string name="action_retry_decryption">"Entschlüsselung erneut versuchen"</string>
|
||||
<string name="action_save">"Speichern"</string>
|
||||
<string name="action_search">"Suchen"</string>
|
||||
<string name="action_send">"Senden"</string>
|
||||
<string name="action_send_message">"Nachricht senden"</string>
|
||||
<string name="action_share">"Teilen"</string>
|
||||
<string name="action_share_link">"Link teilen"</string>
|
||||
<string name="action_skip">"Überspringen"</string>
|
||||
<string name="action_take_photo">"Foto aufnehmen"</string>
|
||||
<string name="action_yes">"Ja"</string>
|
||||
<string name="common_about">"Über"</string>
|
||||
<string name="common_analytics">"Analytik"</string>
|
||||
<string name="common_audio">"Audio"</string>
|
||||
<string name="common_bubbles">"Blasen"</string>
|
||||
<string name="common_decryption_error">"Entschlüsselungsfehler"</string>
|
||||
<string name="common_developer_options">"Entwickleroptionen"</string>
|
||||
<string name="common_edited_suffix">"(bearbeitet)"</string>
|
||||
<string name="common_encryption_enabled">"Verschlüsselung aktiviert"</string>
|
||||
<string name="common_error">"Fehler"</string>
|
||||
<string name="common_file">"Datei"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Bild"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Link in Zwischenablage kopiert"</string>
|
||||
<string name="common_message">"Nachricht"</string>
|
||||
<string name="common_modern">"Modern"</string>
|
||||
<string name="common_offline">"Offline"</string>
|
||||
<string name="common_password">"Passwort"</string>
|
||||
<string name="common_reactions">"Reaktionen"</string>
|
||||
<string name="common_security">"Sicherheit"</string>
|
||||
<string name="common_settings">"Einstellungen"</string>
|
||||
<string name="common_sticker">"Sticker"</string>
|
||||
<string name="common_success">"Erfolg"</string>
|
||||
<string name="common_suggestions">"Vorschläge"</string>
|
||||
<string name="common_topic">"Thema"</string>
|
||||
<string name="common_unable_to_decrypt">"Entschlüsselung nicht möglich"</string>
|
||||
<string name="common_unsupported_event">"Nicht unterstütztes Ereignis"</string>
|
||||
<string name="common_username">"Benutzername"</string>
|
||||
<string name="common_verification_cancelled">"Verifizierung abgebrochen"</string>
|
||||
<string name="common_verification_complete">"Verifizierung abgeschlossen"</string>
|
||||
<string name="common_video">"Video"</string>
|
||||
<string name="common_waiting">"Warten…"</string>
|
||||
<string name="dialog_title_warning">"Warnung"</string>
|
||||
<string name="emoji_picker_category_activity">"Aktivitäten"</string>
|
||||
<string name="emoji_picker_category_flags">"Flaggen"</string>
|
||||
<string name="emoji_picker_category_foods">"Essen & Trinken"</string>
|
||||
<string name="emoji_picker_category_nature">"Tiere & Natur"</string>
|
||||
<string name="emoji_picker_category_objects">"Objekte"</string>
|
||||
<string name="emoji_picker_category_people">"Smileys & Personen"</string>
|
||||
<string name="emoji_picker_category_places">"Reisen & Orte"</string>
|
||||
<string name="emoji_picker_category_symbols">"Symbole"</string>
|
||||
<string name="error_failed_loading_messages">"Fehler beim Laden der Nachrichten"</string>
|
||||
<string name="error_unknown">"Entschuldigung, ein Fehler ist aufgetreten."</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<plurals name="common_member_count">
|
||||
<item quantity="one">"%1$d Mitglied"</item>
|
||||
<item quantity="other">"%1$d Mitglieder"</item>
|
||||
</plurals>
|
||||
<string name="report_content_hint">"Grund für die Meldung dieses Inhalts"</string>
|
||||
<string name="room_timeline_beginning_of_room">"Dies ist der Anfang von %1$s."</string>
|
||||
<string name="room_timeline_read_marker_title">"Neu"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blockieren"</string>
|
||||
<string name="screen_room_member_details_block_user">"Nutzer blockieren"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Blockierung aufheben"</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Nutzer entblockieren"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Erkennungsschwelle"</string>
|
||||
<string name="settings_version_number">"Version: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"de"</string>
|
||||
<string name="dialog_title_error">"Fehler"</string>
|
||||
<string name="dialog_title_success">"Erfolg"</string>
|
||||
<string name="screen_report_content_block_user">"Nutzer blockieren"</string>
|
||||
</resources>
|
||||
|
|
@ -94,8 +94,6 @@
|
|||
<string name="common_video">"Vídeo"</string>
|
||||
<string name="common_waiting">"Esperando…"</string>
|
||||
<string name="dialog_title_confirmation">"Confirmar"</string>
|
||||
<string name="dialog_title_error">"Error"</string>
|
||||
<string name="dialog_title_success">"Terminado"</string>
|
||||
<string name="dialog_title_warning">"Atención"</string>
|
||||
<string name="emoji_picker_category_activity">"Actividades"</string>
|
||||
<string name="emoji_picker_category_flags">"Banderas"</string>
|
||||
|
|
@ -129,7 +127,6 @@
|
|||
<string name="room_timeline_beginning_of_room">"Este es el principio de %1$s."</string>
|
||||
<string name="room_timeline_beginning_of_room_no_name">"Este es el principio de esta conversación."</string>
|
||||
<string name="room_timeline_read_marker_title">"Nuevos"</string>
|
||||
<string name="screen_report_content_block_user">"Bloquear usuario"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Marque si quieres ocultar todos los mensajes actuales y futuros de este usuario"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Bloquear"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento."</string>
|
||||
|
|
@ -142,4 +139,7 @@
|
|||
<string name="settings_title_general">"General"</string>
|
||||
<string name="settings_version_number">"Versión: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"es"</string>
|
||||
<string name="dialog_title_error">"Error"</string>
|
||||
<string name="dialog_title_success">"Terminado"</string>
|
||||
<string name="screen_report_content_block_user">"Bloquear usuario"</string>
|
||||
</resources>
|
||||
|
|
@ -94,8 +94,6 @@
|
|||
<string name="common_video">"Video"</string>
|
||||
<string name="common_waiting">"In attesa…"</string>
|
||||
<string name="dialog_title_confirmation">"Conferma"</string>
|
||||
<string name="dialog_title_error">"Errore"</string>
|
||||
<string name="dialog_title_success">"Operazione riuscita"</string>
|
||||
<string name="dialog_title_warning">"Attenzione"</string>
|
||||
<string name="emoji_picker_category_activity">"Attività"</string>
|
||||
<string name="emoji_picker_category_flags">"Bandiere"</string>
|
||||
|
|
@ -129,7 +127,6 @@
|
|||
<string name="room_timeline_beginning_of_room">"Questo è l\'inizio di %1$s."</string>
|
||||
<string name="room_timeline_beginning_of_room_no_name">"Questo è l\'inizio della conversazione."</string>
|
||||
<string name="room_timeline_read_marker_title">"Nuovo"</string>
|
||||
<string name="screen_report_content_block_user">"Blocca utente"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blocca"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento."</string>
|
||||
|
|
@ -142,4 +139,7 @@
|
|||
<string name="settings_title_general">"Generali"</string>
|
||||
<string name="settings_version_number">"Versione: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"it"</string>
|
||||
<string name="dialog_title_error">"Errore"</string>
|
||||
<string name="dialog_title_success">"Operazione riuscita"</string>
|
||||
<string name="screen_report_content_block_user">"Blocca utente"</string>
|
||||
</resources>
|
||||
|
|
@ -4,8 +4,10 @@
|
|||
<string name="a11y_send_files">"Trimiteți fișiere"</string>
|
||||
<string name="a11y_show_password">"Afișați parola"</string>
|
||||
<string name="a11y_user_menu">"Meniu utilizator"</string>
|
||||
<string name="action_accept">"Acceptați"</string>
|
||||
<string name="action_back">"Înapoi"</string>
|
||||
<string name="action_cancel">"Anulați"</string>
|
||||
<string name="action_choose_photo">"Alegeți o fotografie"</string>
|
||||
<string name="action_clear">"Ștergeți"</string>
|
||||
<string name="action_close">"Închideți"</string>
|
||||
<string name="action_complete_verification">"Verificare completă"</string>
|
||||
|
|
@ -13,13 +15,16 @@
|
|||
<string name="action_continue">"Continuați"</string>
|
||||
<string name="action_copy">"Copiați"</string>
|
||||
<string name="action_copy_link">"Copiați linkul"</string>
|
||||
<string name="action_create">"Creați"</string>
|
||||
<string name="action_create_a_room">"Creați o cameră"</string>
|
||||
<string name="action_decline">"Refuzați"</string>
|
||||
<string name="action_disable">"Dezactivați"</string>
|
||||
<string name="action_done">"Efectuat"</string>
|
||||
<string name="action_edit">"Editați"</string>
|
||||
<string name="action_enable">"Activați"</string>
|
||||
<string name="action_invite">"Invitați"</string>
|
||||
<string name="action_invite_friends_to_app">"Invitați prieteni în %1$s"</string>
|
||||
<string name="action_invites_list">"Invitații"</string>
|
||||
<string name="action_learn_more">"Aflați mai multe"</string>
|
||||
<string name="action_leave">"Părăsiți"</string>
|
||||
<string name="action_leave_room">"Părăsiți camera"</string>
|
||||
|
|
@ -38,15 +43,18 @@
|
|||
<string name="action_save">"Salvați"</string>
|
||||
<string name="action_search">"Căutați"</string>
|
||||
<string name="action_send">"Trimiteți"</string>
|
||||
<string name="action_send_message">"Trimiteți mesajul"</string>
|
||||
<string name="action_share">"Partajați"</string>
|
||||
<string name="action_share_link">"Partajați linkul"</string>
|
||||
<string name="action_skip">"Omiteți"</string>
|
||||
<string name="action_start">"Începeți"</string>
|
||||
<string name="action_start_chat">"Începeți discuția"</string>
|
||||
<string name="action_start_verification">"Începeți verificarea"</string>
|
||||
<string name="action_take_photo">"Faceți o fotografie"</string>
|
||||
<string name="action_view_source">"Vedeți sursă"</string>
|
||||
<string name="action_yes">"Da"</string>
|
||||
<string name="common_about">"Despre"</string>
|
||||
<string name="common_analytics">"Analitice"</string>
|
||||
<string name="common_audio">"Audio"</string>
|
||||
<string name="common_bubbles">"Baloane"</string>
|
||||
<string name="common_creating_room">"Se creează camera…"</string>
|
||||
|
|
@ -94,8 +102,6 @@
|
|||
<string name="common_video">"Video"</string>
|
||||
<string name="common_waiting">"Se aşteaptă…"</string>
|
||||
<string name="dialog_title_confirmation">"Confirmare"</string>
|
||||
<string name="dialog_title_error">"Eroare"</string>
|
||||
<string name="dialog_title_success">"Succes"</string>
|
||||
<string name="dialog_title_warning">"Avertisment"</string>
|
||||
<string name="emoji_picker_category_activity">"Activități"</string>
|
||||
<string name="emoji_picker_category_flags">"Steaguri"</string>
|
||||
|
|
@ -131,7 +137,17 @@
|
|||
<string name="room_timeline_beginning_of_room">"Acesta este începutul conversației %1$s."</string>
|
||||
<string name="room_timeline_beginning_of_room_no_name">"Acesta este începutul acestei conversații."</string>
|
||||
<string name="room_timeline_read_marker_title">"Nou"</string>
|
||||
<string name="screen_report_content_block_user">"Blocați utilizatorul"</string>
|
||||
<string name="screen_analytics_help_us_improve">"Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime."</string>
|
||||
<string name="screen_analytics_prompt_data_usage"><b>"Nu"</b>" înregistrăm sau profilăm datele contului"</string>
|
||||
<string name="screen_analytics_prompt_help_us_improve">"Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime."</string>
|
||||
<string name="screen_analytics_prompt_read_terms">"Puteți citi toate condițiile noastre %1$s."</string>
|
||||
<string name="screen_analytics_prompt_read_terms_content_link">"aici"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Puteți dezactiva această opțiune oricând din setări"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing"><b>"Nu"</b>" împărtășim informații cu terți"</string>
|
||||
<string name="screen_analytics_prompt_title">"Ajutați la îmbunătățirea %1$s"</string>
|
||||
<string name="screen_analytics_read_terms">"Puteți citi toate condițiile noastre %1$s."</string>
|
||||
<string name="screen_analytics_read_terms_content_link">"aici"</string>
|
||||
<string name="screen_analytics_share_data">"Partajați datele analitice"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blocați"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."</string>
|
||||
|
|
@ -144,4 +160,7 @@
|
|||
<string name="settings_title_general">"General"</string>
|
||||
<string name="settings_version_number">"Versiunea: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"ro"</string>
|
||||
<string name="dialog_title_error">"Eroare"</string>
|
||||
<string name="dialog_title_success">"Succes"</string>
|
||||
<string name="screen_report_content_block_user">"Blocați utilizatorul"</string>
|
||||
</resources>
|
||||
|
|
@ -102,8 +102,6 @@
|
|||
<string name="common_video">"Video"</string>
|
||||
<string name="common_waiting">"Waiting…"</string>
|
||||
<string name="dialog_title_confirmation">"Confirmation"</string>
|
||||
<string name="dialog_title_error">"Error"</string>
|
||||
<string name="dialog_title_success">"Success"</string>
|
||||
<string name="dialog_title_warning">"Warning"</string>
|
||||
<string name="emoji_picker_category_activity">"Activities"</string>
|
||||
<string name="emoji_picker_category_flags">"Flags"</string>
|
||||
|
|
@ -122,60 +120,15 @@
|
|||
<string name="leave_room_alert_private_subtitle">"Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite."</string>
|
||||
<string name="leave_room_alert_subtitle">"Are you sure that you want to leave the room?"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="notification_channel_call">"Call"</string>
|
||||
<string name="notification_channel_listening_for_events">"Listening for events"</string>
|
||||
<string name="notification_channel_noisy">"Noisy notifications"</string>
|
||||
<string name="notification_channel_silent">"Silent notifications"</string>
|
||||
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
|
||||
<string name="notification_invitation_action_join">"Join"</string>
|
||||
<string name="notification_invitation_action_reject">"Reject"</string>
|
||||
<string name="notification_new_messages">"New Messages"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Mark as read"</string>
|
||||
<string name="notification_room_action_quick_reply">"Quick reply"</string>
|
||||
<string name="notification_sender_me">"Me"</string>
|
||||
<string name="notification_test_push_notification_content">"You are viewing the notification! Click me!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s and %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room">"%1$s in %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s in %2$s and %3$s"</string>
|
||||
<plurals name="common_member_count">
|
||||
<item quantity="one">"%1$d member"</item>
|
||||
<item quantity="other">"%1$d members"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d message"</item>
|
||||
<item quantity="other">"%1$s: %2$d messages"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_compat_summary_title">
|
||||
<item quantity="one">"%d notification"</item>
|
||||
<item quantity="other">"%d notifications"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="one">"%d invitation"</item>
|
||||
<item quantity="other">"%d invitations"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
<item quantity="one">"%d new message"</item>
|
||||
<item quantity="other">"%d new messages"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
<item quantity="one">"%d unread notified message"</item>
|
||||
<item quantity="other">"%d unread notified messages"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="one">"%d room"</item>
|
||||
<item quantity="other">"%d rooms"</item>
|
||||
</plurals>
|
||||
<plurals name="room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d room change"</item>
|
||||
<item quantity="other">"%1$d room changes"</item>
|
||||
</plurals>
|
||||
<string name="preference_rageshake">"Rageshake to report bug"</string>
|
||||
<string name="push_choose_distributor_dialog_title_android">"Choose how to receive notifications"</string>
|
||||
<string name="push_distributor_background_sync_android">"Background synchronization"</string>
|
||||
<string name="push_distributor_firebase_android">"Google Services"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"No valid Google Play Services found. Notifications may not work properly."</string>
|
||||
<string name="rageshake_dialog_content">"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"</string>
|
||||
<string name="report_content_explanation">"This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."</string>
|
||||
<string name="report_content_hint">"Reason for reporting this content"</string>
|
||||
|
|
@ -193,7 +146,6 @@
|
|||
<string name="screen_analytics_read_terms">"You can read all our terms %1$s."</string>
|
||||
<string name="screen_analytics_read_terms_content_link">"here"</string>
|
||||
<string name="screen_analytics_share_data">"Share analytics data"</string>
|
||||
<string name="screen_report_content_block_user">"Block user"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Block"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
|
||||
|
|
@ -207,4 +159,7 @@
|
|||
<string name="settings_version_number">"Version: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"en"</string>
|
||||
<string name="test_untranslated_default_language_identifier">"en"</string>
|
||||
<string name="dialog_title_error">"Error"</string>
|
||||
<string name="dialog_title_success">"Success"</string>
|
||||
<string name="screen_report_content_block_user">"Block user"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d8c78efed997712873719636ff1f8479d38d317e443c5d4340346d4328de9c0d
|
||||
size 28744
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d8c78efed997712873719636ff1f8479d38d317e443c5d4340346d4328de9c0d
|
||||
size 28744
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2f62537b4aa79f501908d1fff9d269139e88db5f6dbaedcf63670a4f71b47bff
|
||||
size 28303
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2f62537b4aa79f501908d1fff9d269139e88db5f6dbaedcf63670a4f71b47bff
|
||||
size 28303
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:354452861d1e006a8bfa744251ffdaf15088e0bb181a53043f121e606233d648
|
||||
size 67340
|
||||
oid sha256:a8a9186d741d251dc8bdf6bcf71d395e8c057c310585aeb8911609eaaacce842
|
||||
size 64550
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f1cb6bac9e72b956d0cffde807340e36a7a4d6873d4d7337995b53e82769c4f9
|
||||
size 68135
|
||||
oid sha256:a8a9186d741d251dc8bdf6bcf71d395e8c057c310585aeb8911609eaaacce842
|
||||
size 64550
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dc56e29c26250c6fef312ec4c5fdfaa2f63159fd0565bd37522b46c7ff67906a
|
||||
size 61924
|
||||
oid sha256:94a589b556a750485fd61af6446457c98c5f112d0d013cd78016b88a8829e6a8
|
||||
size 58643
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c6faff25a1c187dee59d0dcf0affd5029251512efb6eff7fc2d41d34bace2061
|
||||
size 62356
|
||||
oid sha256:94a589b556a750485fd61af6446457c98c5f112d0d013cd78016b88a8829e6a8
|
||||
size 58643
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8bca09418758a20493dae2e73a747449af8a448ad3a3cc4c5aae2e08a425f3fb
|
||||
size 13464
|
||||
oid sha256:b5f2b24a19ca49b3e6e34ccd65d2bdba72d0384104931bda92e191959d58c5c3
|
||||
size 12697
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e50325c75193e47958862ea9cb515d7c84d2c47a00b01256fc244319780c107f
|
||||
size 12425
|
||||
oid sha256:c4fa32eb24a0cc51b9b19c6f24a7d3d59aae65f1f30a43b1a6d70b3ed3e2154d
|
||||
size 11716
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:97b11203623c0c98da88dfedf85cb80d1f35cc55da570e12061b1385691bf1f0
|
||||
size 27758
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0fc272268179409483fc5fad89aa00714a6811c63e478b5c696d4ab0338ce6bf
|
||||
size 37781
|
||||
oid sha256:3cdb131c68de1fce5a3319151e39148e9f3a71c7bc3984e89ec0a80abf0f7288
|
||||
size 37044
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d729c75d73d1837b365c8332b6e4203cb2492f6c5c4af06741a4bd2e818daebb
|
||||
size 60667
|
||||
oid sha256:8b265978c4db7b266fd07d56364eccafba1cd765ed9bf6d5a03b1584e173ba6a
|
||||
size 59936
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0fc272268179409483fc5fad89aa00714a6811c63e478b5c696d4ab0338ce6bf
|
||||
size 37781
|
||||
oid sha256:3cdb131c68de1fce5a3319151e39148e9f3a71c7bc3984e89ec0a80abf0f7288
|
||||
size 37044
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:56a253a7981823ca0fc7b653f9e83d29d1f259fea43ca4cee5760fa863306f2c
|
||||
size 39847
|
||||
oid sha256:ccf989dac7fad3cc70443d96e1ebd519463a6559ed0795ae2a1ffbaf91bdfe7c
|
||||
size 39092
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2b1222e1ef2d0739caa410540bf74fea681ef2e05bb04b976e50f1fe5256d613
|
||||
size 39762
|
||||
oid sha256:c2a23141c6cc8aa6e7e5f0757bda4d1117bf7f752412ccc4649ea560113b3e3f
|
||||
size 39030
|
||||
|
|
|
|||
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