Merge branch 'develop' into feature/fga/some_room_related_fixes

This commit is contained in:
ganfra 2023-04-27 17:25:12 +02:00
commit 40f3f2873b
109 changed files with 1395 additions and 460 deletions

View file

@ -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.

View file

@ -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
View file

@ -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>

View file

@ -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

View file

@ -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:

View file

@ -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
View file

@ -0,0 +1 @@
Block & unblock users from room details screen.

1
changelog.d/354.feature Normal file
View file

@ -0,0 +1 @@
Improve room list search and general UI

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -4,17 +4,17 @@
<string name="screen_change_server_error_no_sliding_sync_message">"This server currently doesnt 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>

View file

@ -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>

View file

@ -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,

View file

@ -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(

View file

@ -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)

View file

@ -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>

View file

@ -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>

View file

@ -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
)
}

View file

@ -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,
)
}

View file

@ -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 {

View file

@ -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
)

View file

@ -71,5 +71,6 @@ fun aRoomDetailsState() = RoomDetailsState(
displayLeaveRoomWarning = null,
error = null,
roomType = RoomDetailsType.Room,
roomMemberDetailsState = null,
eventSink = {}
)

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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) {

View file

@ -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()) {

View file

@ -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
}

View file

@ -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() {

View file

@ -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 }
}
}

View file

@ -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
}
}

View file

@ -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 = {},
)

View file

@ -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) =

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 {

View file

@ -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()
}
}
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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
)

View file

@ -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 = {}
)

View file

@ -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 = {})
}
}

View file

@ -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(

View file

@ -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)
}
}
}
}
}

View file

@ -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>

View file

@ -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()
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 doesnt 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 dont 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>

View file

@ -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 }

View file

@ -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),

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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)
)
}

View file

@ -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>

View file

@ -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 }
}
}

View file

@ -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)

View file

@ -23,6 +23,7 @@ android {
}
dependencies {
api(projects.libraries.core)
api(projects.libraries.matrix.api)
api(libs.coroutines.core)
}

View file

@ -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
}
}

View file

@ -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(

View 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>

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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")
}

View file

@ -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>

View file

@ -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 &amp; Trinken"</string>
<string name="emoji_picker_category_nature">"Tiere &amp; Natur"</string>
<string name="emoji_picker_category_objects">"Objekte"</string>
<string name="emoji_picker_category_people">"Smileys &amp; Personen"</string>
<string name="emoji_picker_category_places">"Reisen &amp; 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 homeservers 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>

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d8c78efed997712873719636ff1f8479d38d317e443c5d4340346d4328de9c0d
size 28744

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d8c78efed997712873719636ff1f8479d38d317e443c5d4340346d4328de9c0d
size 28744

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2f62537b4aa79f501908d1fff9d269139e88db5f6dbaedcf63670a4f71b47bff
size 28303

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2f62537b4aa79f501908d1fff9d269139e88db5f6dbaedcf63670a4f71b47bff
size 28303

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:354452861d1e006a8bfa744251ffdaf15088e0bb181a53043f121e606233d648
size 67340
oid sha256:a8a9186d741d251dc8bdf6bcf71d395e8c057c310585aeb8911609eaaacce842
size 64550

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f1cb6bac9e72b956d0cffde807340e36a7a4d6873d4d7337995b53e82769c4f9
size 68135
oid sha256:a8a9186d741d251dc8bdf6bcf71d395e8c057c310585aeb8911609eaaacce842
size 64550

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc56e29c26250c6fef312ec4c5fdfaa2f63159fd0565bd37522b46c7ff67906a
size 61924
oid sha256:94a589b556a750485fd61af6446457c98c5f112d0d013cd78016b88a8829e6a8
size 58643

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c6faff25a1c187dee59d0dcf0affd5029251512efb6eff7fc2d41d34bace2061
size 62356
oid sha256:94a589b556a750485fd61af6446457c98c5f112d0d013cd78016b88a8829e6a8
size 58643

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8bca09418758a20493dae2e73a747449af8a448ad3a3cc4c5aae2e08a425f3fb
size 13464
oid sha256:b5f2b24a19ca49b3e6e34ccd65d2bdba72d0384104931bda92e191959d58c5c3
size 12697

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e50325c75193e47958862ea9cb515d7c84d2c47a00b01256fc244319780c107f
size 12425
oid sha256:c4fa32eb24a0cc51b9b19c6f24a7d3d59aae65f1f30a43b1a6d70b3ed3e2154d
size 11716

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97b11203623c0c98da88dfedf85cb80d1f35cc55da570e12061b1385691bf1f0
size 27758

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0fc272268179409483fc5fad89aa00714a6811c63e478b5c696d4ab0338ce6bf
size 37781
oid sha256:3cdb131c68de1fce5a3319151e39148e9f3a71c7bc3984e89ec0a80abf0f7288
size 37044

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d729c75d73d1837b365c8332b6e4203cb2492f6c5c4af06741a4bd2e818daebb
size 60667
oid sha256:8b265978c4db7b266fd07d56364eccafba1cd765ed9bf6d5a03b1584e173ba6a
size 59936

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0fc272268179409483fc5fad89aa00714a6811c63e478b5c696d4ab0338ce6bf
size 37781
oid sha256:3cdb131c68de1fce5a3319151e39148e9f3a71c7bc3984e89ec0a80abf0f7288
size 37044

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:56a253a7981823ca0fc7b653f9e83d29d1f259fea43ca4cee5760fa863306f2c
size 39847
oid sha256:ccf989dac7fad3cc70443d96e1ebd519463a6559ed0795ae2a1ffbaf91bdfe7c
size 39092

View file

@ -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