diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3fdc5586c2..be8d1a370c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -34,7 +34,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Configure gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/gradle-build-action@v2.4.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK
diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml
index 72f9aee8f1..13e0c3b4ad 100644
--- a/.github/workflows/maestro.yml
+++ b/.github/workflows/maestro.yml
@@ -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.
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index dafa94c7b1..e668c17275 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -31,7 +31,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Configure gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/gradle-build-action@v2.4.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run code quality check suite
@@ -79,7 +79,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Configure gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/gradle-build-action@v2.4.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Dependency analysis
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 7646de42a0..391c6c6b21 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -34,7 +34,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Configure gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/gradle-build-action@v2.4.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml
index 136acfe491..d7951a012b 100644
--- a/.github/workflows/triage-labelled.yml
+++ b/.github/workflows/triage-labelled.yml
@@ -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 }}
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 0fc3113136..69e86158ba 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.maestro/tests/account/logout.yaml b/.maestro/tests/account/logout.yaml
index 3c9dd07972..a06ac25e2d 100644
--- a/.maestro/tests/account/logout.yaml
+++ b/.maestro/tests/account/logout.yaml
@@ -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
diff --git a/.maestro/tests/settings/settings.yaml b/.maestro/tests/settings/settings.yaml
index 397a0f70b5..ee3104024c 100644
--- a/.maestro/tests/settings/settings.yaml
+++ b/.maestro/tests/settings/settings.yaml
@@ -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:
diff --git a/anvilcodegen/build.gradle.kts b/anvilcodegen/build.gradle.kts
index 5dbfbec73a..0edb358423 100644
--- a/anvilcodegen/build.gradle.kts
+++ b/anvilcodegen/build.gradle.kts
@@ -23,7 +23,7 @@ dependencies {
implementation(projects.anvilannotations)
api(libs.anvil.compiler.api)
implementation(libs.anvil.compiler.utils)
- implementation("com.squareup:kotlinpoet:1.13.0")
+ implementation("com.squareup:kotlinpoet:1.13.1")
implementation(libs.dagger)
compileOnly("com.google.auto.service:auto-service-annotations:1.0.1")
kapt("com.google.auto.service:auto-service:1.0.1")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 342e05532c..4a81d52d46 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -28,6 +28,16 @@
android:supportsRtl="true"
android:theme="@style/Theme.ElementX"
tools:targetApi="33">
+
+
+
+
+
+
+
+
+
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt
index 22929e3d66..2b33e37c7e 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt
@@ -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
@@ -41,7 +42,6 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.services.appnavstate.api.AppNavigationStateService
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -102,7 +102,7 @@ class RoomFlowNode @AssistedInject constructor(
private fun fetchRoomMembers() = lifecycleScope.launch {
val room = inputs.room
- room.fetchMembers()
+ room.updateMembers()
.onFailure {
Timber.e(it, "Fail to fetch members for room ${room.roomId}")
}.onSuccess {
@@ -134,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,
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt
index ef611a2f4b..a151be665c 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt
@@ -24,7 +24,6 @@ import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.activeElement
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
-import com.bumble.appyx.testing.unit.common.helper.nodeTestHelper
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
import com.google.common.truth.Truth
import io.element.android.features.messages.api.MessagesEntryPoint
@@ -81,19 +80,6 @@ class RoomFlowNodeTest {
roomMembershipObserver = RoomMembershipObserver()
)
- @Test
- fun `given a room flow node when initialized then it fetches room members`() {
- // GIVEN
- val room = FakeMatrixRoom()
- val inputs = RoomFlowNode.Inputs(room)
- val roomFlowNode = aRoomFlowNode(listOf(inputs))
- Truth.assertThat(room.areMembersFetched).isFalse()
- // WHEN
- roomFlowNode.nodeTestHelper()
- // THEN
- Truth.assertThat(room.areMembersFetched).isTrue()
- }
-
@Test
fun `given a room flow node when initialized then it loads messages entry point`() {
// GIVEN
diff --git a/changelog.d/339.feature b/changelog.d/339.feature
new file mode 100644
index 0000000000..4cbf834b1c
--- /dev/null
+++ b/changelog.d/339.feature
@@ -0,0 +1 @@
+Block & unblock users from room details screen.
diff --git a/changelog.d/354.feature b/changelog.d/354.feature
new file mode 100644
index 0000000000..7d6e15b545
--- /dev/null
+++ b/changelog.d/354.feature
@@ -0,0 +1 @@
+Improve room list search and general UI
diff --git a/features/createroom/impl/src/main/res/values-de/translations.xml b/features/createroom/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..8a3c6cdeda
--- /dev/null
+++ b/features/createroom/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,7 @@
+
+
+ "Neuer Raum"
+ "Privater Raum (nur auf Einladung)"
+ "Raumname"
+ "Thema (optional)"
+
\ No newline at end of file
diff --git a/features/createroom/impl/src/main/res/values-ro/translations.xml b/features/createroom/impl/src/main/res/values-ro/translations.xml
index af6e3db1fa..a1ea3b31f0 100644
--- a/features/createroom/impl/src/main/res/values-ro/translations.xml
+++ b/features/createroom/impl/src/main/res/values-ro/translations.xml
@@ -3,6 +3,16 @@
"Cameră nouă"
"Invitați persoane"
"Adaugați persoane"
+ "A apărut o eroare la crearea camerei"
+ "Mesajele din această cameră sunt criptate. Criptarea nu poate fi dezactivată ulterior."
+ "Cameră privată (doar pe bază de invitație)"
+ "Mesajele nu sunt criptate și oricine le poate citi. Puteți activa criptarea la o dată ulterioară."
+ "Cameră publică (oricine)"
+ "Numele camerei"
+ "e.g. Mici și Cozonaci"
+ "Creați o cameră"
+ "Subiect (opțional)"
+ "Despre ce este această cameră?"
"A apărut o eroare la încercarea începerii conversației"
"Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită."
\ No newline at end of file
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt
index 64d59b221c..db7cd06170 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt
+++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt
@@ -102,7 +102,7 @@ class CreateRoomRootPresenterTests {
}.test {
val initialState = awaitItem()
val matrixUser = MatrixUser(UserId("@name:domain"))
- val fakeDmResult = FakeMatrixRoom(RoomId("!fakeDmResult:domain"))
+ val fakeDmResult = FakeMatrixRoom(roomId = RoomId("!fakeDmResult:domain"))
fakeMatrixClient.givenFindDmResult(fakeDmResult)
diff --git a/features/invitelist/impl/src/main/res/values-de/translations.xml b/features/invitelist/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..95e63cf5f2
--- /dev/null
+++ b/features/invitelist/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,6 @@
+
+
+ "Chat ablehnen"
+ "Keine Einladungen"
+ "%1$s hat dich eingeladen"
+
\ No newline at end of file
diff --git a/features/invitelist/impl/src/main/res/values-es/translations.xml b/features/invitelist/impl/src/main/res/values-es/translations.xml
new file mode 100644
index 0000000000..49c32a9e49
--- /dev/null
+++ b/features/invitelist/impl/src/main/res/values-es/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "%1$s te invitó."
+
\ No newline at end of file
diff --git a/features/invitelist/impl/src/main/res/values-it/translations.xml b/features/invitelist/impl/src/main/res/values-it/translations.xml
new file mode 100644
index 0000000000..c0eccfcb7c
--- /dev/null
+++ b/features/invitelist/impl/src/main/res/values-it/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "%1$s ti ha invitato"
+
\ No newline at end of file
diff --git a/features/invitelist/impl/src/main/res/values-ro/translations.xml b/features/invitelist/impl/src/main/res/values-ro/translations.xml
new file mode 100644
index 0000000000..026485d102
--- /dev/null
+++ b/features/invitelist/impl/src/main/res/values-ro/translations.xml
@@ -0,0 +1,9 @@
+
+
+ "Sigur doriți să refuzați alăturarea la %1$s?"
+ "Refuzați invitația"
+ "Sigur doriți să refuzați conversațiile cu %1$s?"
+ "Refuzați conversația"
+ "Nicio invitație"
+ "%1$s v-a invitat"
+
\ No newline at end of file
diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..061f3453df
--- /dev/null
+++ b/features/login/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,7 @@
+
+
+ "Wie lautet die Adresse deines Servers?"
+ "Willkommen zurück!"
+ "Passwort"
+ "Benutzername"
+
\ No newline at end of file
diff --git a/features/login/impl/src/main/res/values-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml
index a299083994..284527c2f5 100644
--- a/features/login/impl/src/main/res/values-es/translations.xml
+++ b/features/login/impl/src/main/res/values-es/translations.xml
@@ -4,17 +4,17 @@
"Este servidor no soporta sliding sync."
"Dirección del homeserver"
"Solo puedes conectarte a un servidor que soporte sliding sync. El administrador de tu servidor tendrá que configurarlo. %1$s"
- "Continuar"
"¿Cuál es la dirección de tu servidor?"
- "Selecciona tu servidor"
"Esta cuenta ha sido desactivada."
"Usuario y/o contraseña incorrectos"
"Este no es un id de usuario válido. Formato esperado: \'@user:homeserver.org\'"
"El servidor seleccionado no admite contraseñas ni inicio de sesión OIDC. Póngase en contacto con su administrador o elija otro homeserver."
"Introduce tus datos"
- "Contraseña"
"Donde viven tus conversaciones"
- "Continuar"
"¡Hola de nuevo!"
+ "Continuar"
+ "Selecciona tu servidor"
+ "Contraseña"
+ "Continuar"
"Usuario"
\ No newline at end of file
diff --git a/features/login/impl/src/main/res/values-it/translations.xml b/features/login/impl/src/main/res/values-it/translations.xml
index 429f156883..b11875a18e 100644
--- a/features/login/impl/src/main/res/values-it/translations.xml
+++ b/features/login/impl/src/main/res/values-it/translations.xml
@@ -4,17 +4,17 @@
"Questo server attualmente non supporta la sincronizzazione scorrevole."
"URL dell\'homeserver"
"Puoi connetterti solo a un server esistente che supporta la sincronizzazione scorrevole. L\'amministratore del tuo server domestico dovrà configurarlo. %1$s"
- "Continua"
"Qual è l\'indirizzo del tuo server?"
- "Seleziona il tuo server"
"Questo profilo è stato disattivato."
"Nome utente e/o password errati"
"Questo non è un identificatore utente valido. Formato previsto: \'@user:homeserver.org\'"
"L\'homeserver selezionato non supporta la password o l\'accesso OIDC. Contatta il tuo amministratore o scegli un altro homeserver."
"Inserisci i tuoi dati"
- "Password"
"Dove vivono le tue conversazioni"
- "Continua"
"Bentornato!"
+ "Continua"
+ "Seleziona il tuo server"
+ "Password"
+ "Continua"
"Nome utente"
\ No newline at end of file
diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml
index 2b5cce6829..349e3ddc04 100644
--- a/features/login/impl/src/main/res/values-ro/translations.xml
+++ b/features/login/impl/src/main/res/values-ro/translations.xml
@@ -4,17 +4,17 @@
"Momentan acest server nu oferă suport pentru sliding sync."
"Adresa URL a homeserver-ului"
"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"
- "Continuați"
"Care este adresa serverului dumneavoastră?"
- "Selectați serverul"
"Acest cont a fost dezactivat."
"Utilizator și/sau parolă incorecte"
"Acesta nu este un identificator de utilizator valid. Format așteptat: „@user:homeserver.org”"
"Homeserver-ul selectat nu acceptă autentificarea prin parola sau OIDC. Te rugăm să contactezi administratorul sau să alegi un alt homeserver."
"Introduceți detaliile"
- "Parolă"
"Locul unde trăiesc conversațiile tale"
- "Continuați"
"Bine ați revenit!"
+ "Continuați"
+ "Selectați serverul"
+ "Parola"
+ "Continuați"
"Utilizator"
\ No newline at end of file
diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml
index 82ea22e61b..6b0ecee43d 100644
--- a/features/login/impl/src/main/res/values/localazy.xml
+++ b/features/login/impl/src/main/res/values/localazy.xml
@@ -4,17 +4,17 @@
"This server currently doesn’t support sliding sync."
"Homeserver URL"
"You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$s"
- "Continue"
"What is the address of your server?"
- "Select your server"
"This account has been deactivated."
"Incorrect username and/or password"
"This is not a valid user identifier. Expected format: ‘@user:homeserver.org’"
"The selected homeserver doesn\'t support password or OIDC login. Please contact your admin or choose another homeserver."
"Enter your details"
- "Password"
"Where your conversations live"
- "Continue"
"Welcome back!"
+ "Continue"
+ "Select your server"
+ "Password"
+ "Continue"
"Username"
\ No newline at end of file
diff --git a/features/logout/api/src/main/res/values-de/translations.xml b/features/logout/api/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..9fd4f6b083
--- /dev/null
+++ b/features/logout/api/src/main/res/values-de/translations.xml
@@ -0,0 +1,7 @@
+
+
+ "Abmelden"
+ "Abmelden"
+ "Abmeldung läuft…"
+ "Abmelden"
+
\ No newline at end of file
diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts
index 9b43d41d4d..3bd73d5ed2 100644
--- a/features/messages/impl/build.gradle.kts
+++ b/features/messages/impl/build.gradle.kts
@@ -40,6 +40,8 @@ dependencies {
implementation(projects.libraries.textcomposer)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.dateformatter.api)
+ implementation(projects.libraries.mediapickers)
+ implementation(projects.libraries.featureflag.api)
implementation(projects.features.networkmonitor.api)
implementation(libs.coil.compose)
implementation(libs.datetime)
@@ -55,6 +57,8 @@ dependencies {
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.features.networkmonitor.test)
+ testImplementation(projects.tests.testutils)
+ testImplementation(projects.libraries.featureflag.test)
androidTestImplementation(libs.test.junitext)
ksp(libs.showkase.processor)
diff --git a/features/messages/impl/src/androidTest/kotlin/io/element/android/features/messages/ExampleInstrumentedTest.kt b/features/messages/impl/src/androidTest/kotlin/io/element/android/features/messages/ExampleInstrumentedTest.kt
deleted file mode 100644
index 97ef4f5a3b..0000000000
--- a/features/messages/impl/src/androidTest/kotlin/io/element/android/features/messages/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (c) 2022 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package io.element.android.features.messages
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("io.element.android.features.messages.test", appContext.packageName)
- }
-}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt
index dc71d1d834..c2becf71cf 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt
@@ -102,6 +102,7 @@ private fun SheetContent(
// Crashes if sheetContent size is zero
Box(modifier = modifier.size(1.dp))
}
+
is ActionListState.Target.Success -> {
val actions = target.actions
LazyColumn(
@@ -146,5 +147,11 @@ fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) st
@Composable
private fun ContentToPreview(state: ActionListState) {
- SheetContent(state)
+ ActionListView(
+ state = state,
+ modalBottomSheetState = ModalBottomSheetState(
+ initialValue = ModalBottomSheetValue.Expanded
+ ),
+ onActionSelected = { _, _ -> }
+ )
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt
index 1da1188ee4..610f6e1ec0 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt
@@ -24,4 +24,6 @@ sealed interface MessageComposerEvents {
object CloseSpecialMode : MessageComposerEvents
data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents
data class UpdateText(val text: CharSequence) : MessageComposerEvents
+
+ object TakePhoto : MessageComposerEvents
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt
index a5cc266e02..603776e518 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt
@@ -21,23 +21,37 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.core.data.toStableCharSequence
+import io.element.android.libraries.featureflag.api.FeatureFlagService
+import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.mediapickers.PickerProvider
import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import timber.log.Timber
import javax.inject.Inject
class MessageComposerPresenter @Inject constructor(
private val appCoroutineScope: CoroutineScope,
- private val room: MatrixRoom
+ private val room: MatrixRoom,
+ private val mediaPickerProvider: PickerProvider,
+ private val featureFlagService: FeatureFlagService,
) : Presenter {
@Composable
override fun present(): MessageComposerState {
+ val localCoroutineScope = rememberCoroutineScope()
+
+ // Example usage of custom pickers
+ val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { uri ->
+ Timber.d("Photo saved at $uri")
+ })
+
val isFullScreen = rememberSaveable {
mutableStateOf(false)
}
@@ -63,9 +77,14 @@ class MessageComposerPresenter @Inject constructor(
text.value = "".toStableCharSequence()
composerMode.setToNormal()
}
+
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text)
is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode
- }
+ MessageComposerEvents.TakePhoto -> localCoroutineScope.launch {
+ if (featureFlagService.isFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow)) {
+ cameraPhotoPicker.launch()
+ }
+ }}
}
return MessageComposerState(
@@ -92,6 +111,7 @@ class MessageComposerPresenter @Inject constructor(
capturedMode.eventId,
text
)
+
is MessageComposerMode.Quote -> TODO()
is MessageComposerMode.Reply -> room.replyMessage(
capturedMode.eventId,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerView.kt
index e94ebb0a2a..ebdfca6f9e 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerView.kt
@@ -53,6 +53,9 @@ fun MessageComposerView(
composerMode = state.mode,
onCloseSpecialMode = ::onCloseSpecialMode,
onComposerTextChange = ::onComposerTextChange,
+ onAddAttachment = {
+ state.eventSink(MessageComposerEvents.TakePhoto)
+ },
composerCanSendMessage = state.isSendButtonVisible,
composerText = state.text?.charSequence?.toString(),
isInDarkMode = !ElementTheme.colors.isLight,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
index 1d931c76bb..9418e59edc 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
@@ -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,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
index f252494df6..11a3985c9d 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
@@ -99,11 +99,11 @@ fun TimelineView(
itemsIndexed(
items = state.timelineItems,
contentType = { _, timelineItem -> timelineItem.contentType() },
- key = { _, timelineItem -> timelineItem.key() },
+ key = { _, timelineItem -> timelineItem.identifier() },
) { index, timelineItem ->
TimelineItemRow(
timelineItem = timelineItem,
- isHighlighted = timelineItem.key() == state.highlightedEventId?.value,
+ isHighlighted = timelineItem.identifier() == state.highlightedEventId?.value,
onClick = onMessageClicked,
onLongClick = onMessageLongClicked
)
@@ -121,21 +121,6 @@ fun TimelineView(
}
}
-private fun TimelineItem.key(): String {
- return when (this) {
- is TimelineItem.Event -> id
- is TimelineItem.Virtual -> id
- }
-}
-
-private fun TimelineItem.contentType(): Int {
- // Todo optimize for each subtype
- return when (this) {
- is TimelineItem.Event -> 0
- is TimelineItem.Virtual -> 1
- }
-}
-
@Composable
fun TimelineItemRow(
timelineItem: TimelineItem,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/CacheInvalidator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/CacheInvalidator.kt
index c2f3b33667..9aa3ab5e02 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/CacheInvalidator.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/CacheInvalidator.kt
@@ -25,7 +25,7 @@ internal class CacheInvalidator(private val itemStatesCache: MutableList id
}
+ fun contentType(): String = when (this) {
+ is Event -> content.type
+ is Virtual -> model.type
+ }
+
@Immutable
data class Virtual(
val id: String,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt
index 67fefb665e..a059c9b275 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt
@@ -21,4 +21,6 @@ import org.jsoup.nodes.Document
data class TimelineItemEmoteContent(
override val body: String,
override val htmlDocument: Document?
-) : TimelineItemTextBasedContent
+) : TimelineItemTextBasedContent {
+ override val type: String = "TimelineItemEmoteContent"
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContent.kt
index b42c2ac535..ff1bb36faf 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContent.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContent.kt
@@ -20,4 +20,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecry
data class TimelineItemEncryptedContent(
val data: UnableToDecryptContent.Data
-) : TimelineItemEventContent
+) : TimelineItemEventContent {
+ override val type: String = "TimelineItemEncryptedContent"
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt
index 7e166dba97..233f51a5a2 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt
@@ -19,4 +19,6 @@ package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.runtime.Immutable
@Immutable
-sealed interface TimelineItemEventContent
+sealed interface TimelineItemEventContent {
+ val type: String
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt
index 3f00a3c183..d6069e2020 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt
@@ -23,4 +23,6 @@ data class TimelineItemImageContent(
val mediaRequestData: MediaRequestData,
val blurhash: String?,
val aspectRatio: Float
-) : TimelineItemEventContent
+) : TimelineItemEventContent{
+ override val type: String = "TimelineItemImageContent"
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemNoticeContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemNoticeContent.kt
index 6c647158c4..7974f188a3 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemNoticeContent.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemNoticeContent.kt
@@ -21,4 +21,6 @@ import org.jsoup.nodes.Document
data class TimelineItemNoticeContent(
override val body: String,
override val htmlDocument: Document?
-) : TimelineItemTextBasedContent
+) : TimelineItemTextBasedContent {
+ override val type: String = "TimelineItemNoticeContent"
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRedactedContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRedactedContent.kt
index de98b22bbb..7a8edae953 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRedactedContent.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRedactedContent.kt
@@ -16,4 +16,6 @@
package io.element.android.features.messages.impl.timeline.model.event
-object TimelineItemRedactedContent : TimelineItemEventContent
+object TimelineItemRedactedContent : TimelineItemEventContent{
+ override val type: String = "TimelineItemRedactedContent"
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextContent.kt
index 6cbb0ccd08..993275e627 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextContent.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextContent.kt
@@ -21,4 +21,6 @@ import org.jsoup.nodes.Document
data class TimelineItemTextContent(
override val body: String,
override val htmlDocument: Document?
-) : TimelineItemTextBasedContent
+) : TimelineItemTextBasedContent{
+ override val type: String = "TimelineItemTextContent"
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemUnknownContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemUnknownContent.kt
index eb79b29f79..44aeb93e3b 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemUnknownContent.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemUnknownContent.kt
@@ -16,4 +16,6 @@
package io.element.android.features.messages.impl.timeline.model.event
-object TimelineItemUnknownContent : TimelineItemEventContent
+object TimelineItemUnknownContent : TimelineItemEventContent {
+ override val type: String = "TimelineItemUnknownContent"
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModel.kt
index b4c1235f8f..54e95b7294 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModel.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModel.kt
@@ -18,4 +18,6 @@ package io.element.android.features.messages.impl.timeline.model.virtual
data class TimelineItemDaySeparatorModel(
val formattedDate: String
-) : TimelineItemVirtualModel
+) : TimelineItemVirtualModel {
+ override val type: String = "TimelineItemDaySeparatorModel"
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingModel.kt
index 4870177a84..9cc7280b07 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingModel.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingModel.kt
@@ -16,4 +16,6 @@
package io.element.android.features.messages.impl.timeline.model.virtual
-object TimelineItemLoadingModel : TimelineItemVirtualModel
+object TimelineItemLoadingModel : TimelineItemVirtualModel {
+ override val type: String = "TimelineItemLoadingModel"
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemReadMarkerModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemReadMarkerModel.kt
index 247cd58212..0b8e3fc0e5 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemReadMarkerModel.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemReadMarkerModel.kt
@@ -16,4 +16,6 @@
package io.element.android.features.messages.impl.timeline.model.virtual
-object TimelineItemReadMarkerModel : TimelineItemVirtualModel
+object TimelineItemReadMarkerModel : TimelineItemVirtualModel {
+ override val type: String = "TimelineItemReadMarkerModel"
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemTimelineStartModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemTimelineStartModel.kt
index ab0ec4fdf8..8c1afea886 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemTimelineStartModel.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemTimelineStartModel.kt
@@ -16,4 +16,6 @@
package io.element.android.features.messages.impl.timeline.model.virtual
-object TimelineItemTimelineStartModel : TimelineItemVirtualModel
+object TimelineItemTimelineStartModel : TimelineItemVirtualModel {
+ override val type: String = "TimelineItemTimelineStartModel"
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemUnknownVirtualModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemUnknownVirtualModel.kt
index 6d023bf748..8b4fe44744 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemUnknownVirtualModel.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemUnknownVirtualModel.kt
@@ -16,4 +16,6 @@
package io.element.android.features.messages.impl.timeline.model.virtual
-object TimelineItemUnknownVirtualModel : TimelineItemVirtualModel
+object TimelineItemUnknownVirtualModel : TimelineItemVirtualModel {
+ override val type: String = "TimelineItemUnknownVirtualModel"
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemVirtualModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemVirtualModel.kt
index a7911867f9..d6c3529ab4 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemVirtualModel.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemVirtualModel.kt
@@ -19,4 +19,6 @@ package io.element.android.features.messages.impl.timeline.model.virtual
import androidx.compose.runtime.Immutable
@Immutable
-sealed interface TimelineItemVirtualModel
+sealed interface TimelineItemVirtualModel {
+ val type: String
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
index a7bc112174..aa916f1e79 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
@@ -31,10 +31,12 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
+import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
+import io.element.android.libraries.mediapickers.PickerProvider
import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -129,9 +131,10 @@ class MessagesPresenterTest {
): MessagesPresenter {
val messageComposerPresenter = MessageComposerPresenter(
appCoroutineScope = this,
- room = matrixRoom
+ room = matrixRoom,
+ mediaPickerProvider = PickerProvider(isInTest = true),
+ featureFlagService = FakeFeatureFlagService(),
)
-
val timelinePresenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
room = matrixRoom,
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
index 404e50a3bd..2d4ee3842c 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package io.element.android.features.messages.fixtures
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
@@ -31,6 +33,8 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemDaySeparatorFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
+import io.element.android.tests.testutils.testCoroutineDispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
internal fun aTimelineItemsFactory() = TimelineItemsFactory(
dispatchers = testCoroutineDispatchers(),
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt
index 105d2ce31a..b18e9631ef 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt
@@ -27,23 +27,37 @@ import io.element.android.features.messages.impl.textcomposer.MessageComposerEve
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.textcomposer.MessageComposerState
import io.element.android.libraries.core.data.StableCharSequence
+import io.element.android.libraries.featureflag.api.FeatureFlags
+import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_REPLY
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
+import io.element.android.libraries.mediapickers.PickerProvider
import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Test
class MessageComposerPresenterTest {
+
+ private val pickerProvider = PickerProvider(isInTest = true)
+ private val featureFlagService = FakeFeatureFlagService().apply {
+ runBlocking {
+ setFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow, true)
+ }
+ }
+
@Test
fun `present - initial state`() = runTest {
val presenter = MessageComposerPresenter(
this,
- FakeMatrixRoom()
+ FakeMatrixRoom(),
+ pickerProvider,
+ featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -60,7 +74,9 @@ class MessageComposerPresenterTest {
fun `present - toggle fullscreen`() = runTest {
val presenter = MessageComposerPresenter(
this,
- FakeMatrixRoom()
+ FakeMatrixRoom(),
+ pickerProvider,
+ featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -79,7 +95,9 @@ class MessageComposerPresenterTest {
fun `present - change message`() = runTest {
val presenter = MessageComposerPresenter(
this,
- FakeMatrixRoom()
+ FakeMatrixRoom(),
+ pickerProvider,
+ featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -100,7 +118,9 @@ class MessageComposerPresenterTest {
fun `present - change mode to edit`() = runTest {
val presenter = MessageComposerPresenter(
this,
- FakeMatrixRoom()
+ FakeMatrixRoom(),
+ pickerProvider,
+ featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -130,7 +150,9 @@ class MessageComposerPresenterTest {
fun `present - change mode to reply`() = runTest {
val presenter = MessageComposerPresenter(
this,
- FakeMatrixRoom()
+ FakeMatrixRoom(),
+ pickerProvider,
+ featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -150,7 +172,9 @@ class MessageComposerPresenterTest {
fun `present - change mode to quote`() = runTest {
val presenter = MessageComposerPresenter(
this,
- FakeMatrixRoom()
+ FakeMatrixRoom(),
+ pickerProvider,
+ featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -170,7 +194,9 @@ class MessageComposerPresenterTest {
fun `present - send message`() = runTest {
val presenter = MessageComposerPresenter(
this,
- FakeMatrixRoom()
+ FakeMatrixRoom(),
+ pickerProvider,
+ featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -192,7 +218,9 @@ class MessageComposerPresenterTest {
val fakeMatrixRoom = FakeMatrixRoom()
val presenter = MessageComposerPresenter(
this,
- fakeMatrixRoom
+ fakeMatrixRoom,
+ pickerProvider,
+ featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -223,7 +251,9 @@ class MessageComposerPresenterTest {
val fakeMatrixRoom = FakeMatrixRoom()
val presenter = MessageComposerPresenter(
this,
- fakeMatrixRoom
+ fakeMatrixRoom,
+ pickerProvider,
+ featureFlagService,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -248,6 +278,25 @@ class MessageComposerPresenterTest {
assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY)
}
}
+
+ @Test
+ fun `present - Take photo`() = runTest {
+ val fakeMatrixRoom = FakeMatrixRoom()
+ val presenter = MessageComposerPresenter(
+ this,
+ fakeMatrixRoom,
+ pickerProvider,
+ featureFlagService,
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink(MessageComposerEvents.TakePhoto)
+
+ // TODO verify some post processing of the captured image is done
+ }
+ }
}
fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE)
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt
index 24e6f77d32..41f37158d9 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt
@@ -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(
diff --git a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/NetworkMonitorImpl.kt b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/NetworkMonitorImpl.kt
index ba4d6c2775..25819c6eb3 100644
--- a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/NetworkMonitorImpl.kt
+++ b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/NetworkMonitorImpl.kt
@@ -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)
diff --git a/features/onboarding/impl/src/main/res/values-de/translations.xml b/features/onboarding/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..82e01aa522
--- /dev/null
+++ b/features/onboarding/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Sei in deinem Element"
+
\ No newline at end of file
diff --git a/features/rageshake/impl/src/main/res/values-de/translations.xml b/features/rageshake/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..e18f43d1de
--- /dev/null
+++ b/features/rageshake/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,6 @@
+
+
+ "Beschreibe den Fehler…"
+ "Absturzprotokolle senden"
+ "Bildschirmfoto senden"
+
\ No newline at end of file
diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts
index dc840b038f..41f117d6a4 100644
--- a/features/roomdetails/impl/build.gradle.kts
+++ b/features/roomdetails/impl/build.gradle.kts
@@ -53,6 +53,7 @@ dependencies {
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.features.userlist.impl)
testImplementation(projects.features.userlist.test)
+ testImplementation(projects.tests.testutils)
ksp(libs.showkase.processor)
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/blockuser/BlockUserSection.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/blockuser/BlockUserSection.kt
new file mode 100644
index 0000000000..49daa15af3
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/blockuser/BlockUserSection.kt
@@ -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
+ )
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt
index f86b9952a8..f3a0ef9001 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt
@@ -59,7 +59,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
object RoomMemberList : NavTarget
@Parcelize
- data class RoomMemberDetails(val roomMember: RoomMember) : NavTarget
+ data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -74,14 +74,14 @@ class RoomDetailsFlowNode @AssistedInject constructor(
}
NavTarget.RoomMemberList -> {
val roomMemberListCallback = object : RoomMemberListNode.Callback {
- override fun openRoomMemberDetails(roomMember: RoomMember) {
- backstack.push(NavTarget.RoomMemberDetails(roomMember))
+ override fun openRoomMemberDetails(roomMemberId: UserId) {
+ backstack.push(NavTarget.RoomMemberDetails(roomMemberId))
}
}
createNode(buildContext, listOf(roomMemberListCallback))
}
is NavTarget.RoomMemberDetails -> {
- createNode(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMember)))
+ createNode(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMemberId)))
}
}
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt
index 46e76a5f8a..37110e5192 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt
@@ -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,
)
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt
index 45e978b80a..8f96487583 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt
@@ -18,78 +18,70 @@ package io.element.android.features.roomdetails.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
+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.matrix.api.MatrixClient
-import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
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
-import kotlinx.coroutines.Dispatchers
+import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import javax.inject.Inject
class RoomDetailsPresenter @Inject constructor(
- private val sessionId: SessionId,
private val room: MatrixRoom,
private val roomMembershipObserver: RoomMembershipObserver,
+ private val coroutineDispatchers: CoroutineDispatchers,
+ private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory,
) : Presenter {
@Composable
override fun present(): RoomDetailsState {
val coroutineScope = rememberCoroutineScope()
- var leaveRoomWarning by remember {
+ val leaveRoomWarning = remember {
mutableStateOf(null)
}
- var error by remember {
+ val error = remember {
mutableStateOf(null)
}
-
- var memberCount: Async by remember { mutableStateOf(Async.Loading()) }
LaunchedEffect(Unit) {
- withContext(Dispatchers.IO) {
- memberCount = runCatching { room.memberCount() }
- .fold(
- onSuccess = { Async.Success(it) },
- onFailure = { Async.Failure(it) }
- )
- }
+ room.updateMembers()
}
- val dmMember = room.getDmMember()
- val roomType = if (dmMember != null) {
- RoomDetailsType.Dm(dmMember)
- } else {
- RoomDetailsType.Room
- }
+ val membersState by room.membersStateFlow.collectAsState()
+ val memberCount by getMemberCount(membersState)
+ val dmMember by room.getDirectRoomMember(membersState)
+ val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
+ val roomType = getRoomType(dmMember)
fun handleEvents(event: RoomDetailsEvent) {
when (event) {
is RoomDetailsEvent.LeaveRoom -> {
- if (event.needsConfirmation) {
- leaveRoomWarning = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount)
- } else {
- coroutineScope.launch(Dispatchers.IO) {
- room.leave()
- .onSuccess {
- roomMembershipObserver.notifyUserLeftRoom(room.roomId)
- }.onFailure {
- error = RoomDetailsError.AlertGeneric
- }
- leaveRoomWarning = null
- }
- }
+ coroutineScope.leaveRoom(
+ needsConfirmation = event.needsConfirmation,
+ memberCount = memberCount,
+ leaveRoomWarning = leaveRoomWarning,
+ error = error,
+ )
}
- is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning = null
- RoomDetailsEvent.ClearError -> error = null
+ is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning.value = null
+ RoomDetailsEvent.ClearError -> error.value = null
}
}
+ val roomMemberDetailsState = roomMemberDetailsPresenter?.present()
+
return RoomDetailsState(
roomId = room.roomId.value,
roomName = room.name ?: room.displayName,
@@ -98,10 +90,66 @@ class RoomDetailsPresenter @Inject constructor(
roomTopic = room.topic,
memberCount = memberCount,
isEncrypted = room.isEncrypted,
- displayLeaveRoomWarning = leaveRoomWarning,
- error = error,
- roomType = roomType,
+ 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.userId)
+ }
+ }
+
+ @Composable
+ private fun getRoomType(dmMember: RoomMember?): State = remember(dmMember) {
+ derivedStateOf {
+ if (dmMember != null) {
+ RoomDetailsType.Dm(dmMember)
+ } else {
+ RoomDetailsType.Room
+ }
+ }
+ }
+
+ @Composable
+ private fun getMemberCount(membersState: MatrixRoomMembersState): State> {
+ return remember(membersState) {
+ derivedStateOf {
+ when (membersState) {
+ MatrixRoomMembersState.Unknown -> Async.Uninitialized
+ is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size)
+ is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size)
+ is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.size)
+ }
+ }
+ }
+ }
+
+ private fun CoroutineScope.leaveRoom(
+ needsConfirmation: Boolean,
+ memberCount: Async,
+ leaveRoomWarning: MutableState,
+ error: MutableState,
+ ) = launch(coroutineDispatchers.io) {
+ if (needsConfirmation) {
+ leaveRoomWarning.value = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount)
+ } else {
+ room.leave()
+ .onSuccess {
+ roomMembershipObserver.notifyUserLeftRoom(room.roomId)
+ }.onFailure {
+ error.value = RoomDetailsError.AlertGeneric
+ }
+ leaveRoomWarning.value = null
+ }
+ }
}
+
+
+
+
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
index f8fed122de..173ba66ed0 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
@@ -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
)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt
index 61b9d310af..d30ea15f4a 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt
@@ -71,5 +71,6 @@ fun aRoomDetailsState() = RoomDetailsState(
displayLeaveRoomWarning = null,
error = null,
roomType = RoomDetailsType.Room,
+ roomMemberDetailsState = null,
eventSink = {}
)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt
index b9266cd8ca..dbee610dba 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt
@@ -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
@@ -116,7 +117,7 @@ fun RoomDetailsView(
}
if (state.roomType is RoomDetailsType.Room) {
- val memberCount = (state.memberCount as? Async.Success)?.state
+ val memberCount = state.memberCount.dataOrNull()
MembersSection(
memberCount = memberCount,
isLoading = state.memberCount.isLoading(),
@@ -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)
+ }
}
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt
index 68f77b5821..2cd3b7eb08 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt
@@ -20,15 +20,14 @@ 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
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient
+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 io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import javax.inject.Named
@Module
@@ -44,22 +43,14 @@ interface RoomMemberBindsModule {
@ContributesTo(RoomScope::class)
object RoomMemberProvidesModule {
- @Provides
- fun provideRoomDetailsPresenter(
- matrixClient: MatrixClient,
- room: MatrixRoom,
- roomMembershipObserver: RoomMembershipObserver,
- ): RoomDetailsPresenter {
- return RoomDetailsPresenter(matrixClient.sessionId, room, roomMembershipObserver)
- }
-
@Provides
fun provideRoomMemberDetailsPresenterFactory(
+ matrixClient: MatrixClient,
room: MatrixRoom,
): RoomMemberDetailsPresenter.Factory {
return object : RoomMemberDetailsPresenter.Factory {
- override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter {
- return RoomMemberDetailsPresenter(room, roomMember)
+ override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
+ return RoomMemberDetailsPresenter(matrixClient, room, roomMemberId)
}
}
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt
similarity index 56%
rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt
rename to features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt
index 60b0b7cf4b..4534842cac 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt
@@ -14,15 +14,10 @@
* limitations under the License.
*/
-package io.element.android.features.messages.fixtures
+package io.element.android.features.roomdetails.impl.members
-import io.element.android.libraries.core.coroutine.CoroutineDispatchers
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import io.element.android.libraries.matrix.ui.model.MatrixUser
-// TODO Move to common module to reuse
-internal fun testCoroutineDispatchers() = CoroutineDispatchers(
- io = UnconfinedTestDispatcher(),
- computation = UnconfinedTestDispatcher(),
- main = UnconfinedTestDispatcher(),
- diffUpdateDispatcher = UnconfinedTestDispatcher(),
-)
+sealed interface RoomMemberListEvents {
+ data class SelectUser(val user: MatrixUser) : RoomMemberListEvents
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt
index fafe0ede99..5b1e7a72e0 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt
@@ -26,31 +26,25 @@ 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.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
-import io.element.android.libraries.matrix.ui.model.MatrixUser
-import timber.log.Timber
@ContributesNode(RoomScope::class)
class RoomMemberListNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- private val room: MatrixRoom,
private val presenter: RoomMemberListPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
- fun openRoomMemberDetails(roomMember: RoomMember)
+ fun openRoomMemberDetails(roomMemberId: UserId)
}
private val callbacks = plugins()
- private fun onUserSelected(matrixUser: MatrixUser) {
- val member = room.getMember(matrixUser.id)
- if (member != null) {
- callbacks.forEach { it.openRoomMemberDetails(member) }
- } else {
- Timber.e("Could find room member ${matrixUser.id} in room ${room.roomId}")
+ private fun openRoomMemberDetails(roomMemberId: UserId) {
+ callbacks.forEach {
+ it.openRoomMemberDetails(roomMemberId)
}
}
@@ -61,7 +55,7 @@ class RoomMemberListNode @AssistedInject constructor(
state = state,
modifier = modifier,
onBackPressed = { navigateUp() },
- onUserSelected = ::onUserSelected,
+ onMemberSelected = this::openRoomMemberDetails,
)
}
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt
index 8841bd9d5e..8d66d60bc2 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt
@@ -27,10 +27,11 @@ import io.element.android.features.userlist.api.UserListPresenter
import io.element.android.features.userlist.api.UserListPresenterArgs
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Named
@@ -39,6 +40,8 @@ class RoomMemberListPresenter @Inject constructor(
private val userListPresenterFactory: UserListPresenter.Factory,
@Named("RoomMembers") private val userListDataSource: UserListDataSource,
private val userListDataStore: UserListDataStore,
+ private val room: MatrixRoom,
+ private val coroutineDispatchers: CoroutineDispatchers,
) : Presenter {
private val userListPresenter by lazy {
@@ -53,14 +56,16 @@ class RoomMemberListPresenter @Inject constructor(
override fun present(): RoomMemberListState {
val userListState = userListPresenter.present()
val allUsers = remember { mutableStateOf>>(Async.Loading()) }
+
LaunchedEffect(Unit) {
- withContext(Dispatchers.IO) {
+ withContext(coroutineDispatchers.io) {
allUsers.value = Async.Success(userListDataSource.search("").toImmutableList())
}
}
+
return RoomMemberListState(
allUsers = allUsers.value,
- userListState = userListState
+ userListState = userListState,
)
}
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt
index f5e5bd3efb..28885006b1 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt
@@ -18,11 +18,11 @@ package io.element.android.features.roomdetails.impl.members
import io.element.android.features.userlist.api.UserListState
import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class RoomMemberListState(
val allUsers: Async>,
val userListState: UserListState,
-// val eventSink: (AddPeopleEvents) -> Unit,
)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt
index f356e203f2..99c6a9299a 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt
@@ -51,6 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.CenterAlignedT
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.model.MatrixUser
@OptIn(ExperimentalMaterial3Api::class)
@@ -59,8 +60,13 @@ fun RoomMemberListView(
state: RoomMemberListState,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
- onUserSelected: (MatrixUser) -> Unit = {},
+ onMemberSelected: (UserId) -> Unit = {},
) {
+
+ fun onUserSelected(user: MatrixUser) {
+ onMemberSelected(user.id)
+ }
+
Scaffold(
topBar = {
if (!state.userListState.isSearchActive) {
@@ -76,7 +82,7 @@ fun RoomMemberListView(
) {
UserListView(
state = state.userListState,
- onUserSelected = onUserSelected,
+ onUserSelected = ::onUserSelected,
)
if (!state.userListState.isSearchActive) {
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt
index dc0008d2a3..e0559f3caf 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt
@@ -18,26 +18,39 @@ package io.element.android.features.roomdetails.impl.members
import io.element.android.features.userlist.api.UserListDataSource
import io.element.android.libraries.core.bool.orFalse
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.components.avatar.AvatarData
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.roomMembers
import io.element.android.libraries.matrix.ui.model.MatrixUser
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.withContext
import javax.inject.Inject
class RoomUserListDataSource @Inject constructor(
- private val room: MatrixRoom
+ private val room: MatrixRoom,
+ private val coroutineDispatchers: CoroutineDispatchers,
) : UserListDataSource {
- override suspend fun search(query: String): List {
- return room.members().filter { member ->
- if (query.isBlank()) {
- true
- } else {
+ override suspend fun search(query: String): List = withContext(coroutineDispatchers.io) {
+ val roomMembers = room.membersStateFlow
+ .dropWhile { it !is MatrixRoomMembersState.Ready }
+ .first()
+ .roomMembers()
+ .orEmpty()
+ val filteredMembers = if (query.isBlank()) {
+ roomMembers
+ } else {
+ roomMembers.filter { member ->
member.userId.value.contains(query, ignoreCase = true)
|| member.displayName?.contains(query, ignoreCase = true).orFalse()
}
- }.map(::mapMemberToMatrixUser)
+ }
+ filteredMembers.map(::mapMemberToMatrixUser)
}
override suspend fun getProfile(userId: UserId): MatrixUser? {
@@ -55,5 +68,4 @@ class RoomUserListDataSource @Inject constructor(
)
)
}
-
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt
index 2c74caa8fd..5848561f3e 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt
@@ -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
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt
index 5cd2544537..7fd4dd3876 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt
@@ -32,7 +32,6 @@ 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
import io.element.android.libraries.androidutils.R as AndroidUtilsR
@@ -44,19 +43,18 @@ class RoomMemberDetailsNode @AssistedInject constructor(
) : Node(buildContext, plugins = plugins) {
data class Inputs(
- val member: RoomMember,
+ val roomMemberId: UserId,
) : NodeInputs
private val inputs = inputs()
- private val presenter = presenterFactory.create(inputs.member)
+ private val presenter = presenterFactory.create(inputs.roomMemberId)
@Composable
override fun View(modifier: Modifier) {
-
val context = LocalContext.current
fun onShareUser() {
- val permalinkResult = PermalinkBuilder.permalinkForUser(inputs.member.userId)
+ val permalinkResult = PermalinkBuilder.permalinkForUser(inputs.roomMemberId)
permalinkResult.onSuccess { permalink ->
startSharePlainTextIntent(
context = context,
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt
index de24b5ee1b..594152e241 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt
@@ -17,49 +17,108 @@
package io.element.android.features.roomdetails.impl.members.details
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+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.core.bool.orFalse
+import io.element.android.libraries.matrix.api.MatrixClient
+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 io.element.android.libraries.matrix.ui.room.getRoomMember
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
class RoomMemberDetailsPresenter @AssistedInject constructor(
+ private val client: MatrixClient,
private val room: MatrixRoom,
- @Assisted private val roomMember: RoomMember,
+ @Assisted private val roomMemberId: UserId,
) : Presenter {
interface Factory {
- fun create(roomMember: RoomMember): RoomMemberDetailsPresenter
+ fun create(roomMemberId: UserId): RoomMemberDetailsPresenter
}
@Composable
override fun present(): RoomMemberDetailsState {
+ val coroutineScope = rememberCoroutineScope()
+ var confirmationDialog by remember { mutableStateOf(null) }
+ val roomMember by room.getRoomMember(roomMemberId)
+ // the room member is not really live...
+ val isBlocked = remember {
+ mutableStateOf(roomMember?.isIgnored.orFalse())
+ }
+ LaunchedEffect(Unit) {
+ room.updateMembers()
+ }
-// 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(roomMemberId, isBlocked)
+ }
+ }
+ is RoomMemberDetailsEvents.UnblockUser -> {
+ if (event.needsConfirmation) {
+ confirmationDialog = ConfirmationDialog.Unblock
+ } else {
+ confirmationDialog = null
+ coroutineScope.unblockUser(roomMemberId, isBlocked)
+ }
+ }
+ RoomMemberDetailsEvents.ClearConfirmationDialog -> confirmationDialog = null
+ }
+ }
- val userName by produceState(initialValue = roomMember.displayName) {
- room.userDisplayName(roomMember.userId).onSuccess { displayName ->
+ val userName by produceState(initialValue = roomMember?.displayName) {
+ room.userDisplayName(roomMemberId).onSuccess { displayName ->
if (displayName != null) value = displayName
}
}
- val userAvatar by produceState(initialValue = roomMember.avatarUrl) {
- room.userAvatarUrl(roomMember.userId).onSuccess { avatarUrl ->
+ val userAvatar by produceState(initialValue = roomMember?.avatarUrl) {
+ room.userAvatarUrl(roomMemberId).onSuccess { avatarUrl ->
if (avatarUrl != null) value = avatarUrl
}
}
return RoomMemberDetailsState(
- userId = roomMember.userId.value,
+ userId = roomMemberId.value,
userName = userName,
avatarUrl = userAvatar,
- isBlocked = roomMember.isIgnored,
-// eventSink = ::handleEvents
+ isBlocked = isBlocked.value,
+ displayConfirmationDialog = confirmationDialog,
+ isCurrentUser = roomMember?.userId == client.sessionId,
+ eventSink = ::handleEvents
)
}
+
+ private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState) = launch {
+ client.ignoreUser(userId)
+ .map {
+ isBlockedState.value = true
+ room.updateMembers()
+ }
+
+ }
+
+ private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState) = launch {
+ client.unignoreUser(userId)
+ .map {
+ isBlockedState.value = false
+ room.updateMembers()
+ }
+ }
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt
index d9e3f949e7..0a2895db09 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt
@@ -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
+ }
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt
index c719ab7a26..d8e7ce5ad3 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt
@@ -24,6 +24,8 @@ open class RoomMemberDetailsStateProvider : PreviewParameterProvider 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) =
diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..7581b585f1
--- /dev/null
+++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,15 @@
+
+
+
+ - "1 Person"
+ - "%1$d Personen"
+
+ "Raum teilen"
+ "Blockieren"
+ "Nutzer blockieren"
+ "Blockierung aufheben"
+ "Nutzer entblockieren"
+ "Raum verlassen"
+ "Sicherheit"
+ "Thema"
+
\ No newline at end of file
diff --git a/features/roomdetails/impl/src/main/res/values-es/translations.xml b/features/roomdetails/impl/src/main/res/values-es/translations.xml
index ba4327000b..58c486d6c3 100644
--- a/features/roomdetails/impl/src/main/res/values-es/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-es/translations.xml
@@ -4,18 +4,18 @@
- "Una persona"
- "%1$d personas"
+ "Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos."
+ "Cifrado de mensajes activado"
+ "Invitar a otras personas"
+ "Compartir sala"
"Bloquear"
- "Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puedes revertir esta acción en cualquier momento."
+ "Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento."
"Bloquear usuario"
"Desbloquear"
"Al desbloquear al usuario, podrás volver a ver todos sus mensajes."
"Desbloquear usuario"
- "Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos."
- "Cifrado de mensajes activado"
- "Invitar a otras personas"
"Salir de la sala"
"Personas"
"Seguridad"
- "Compartir sala"
"Tema"
\ No newline at end of file
diff --git a/features/roomdetails/impl/src/main/res/values-it/translations.xml b/features/roomdetails/impl/src/main/res/values-it/translations.xml
index 9a980b79a9..a2e61a329c 100644
--- a/features/roomdetails/impl/src/main/res/values-it/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml
@@ -4,18 +4,18 @@
- "1 persona"
- "%1$d persone"
+ "I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli."
+ "Crittografia messaggi abilitata"
+ "Invita persone"
+ "Condividi stanza"
"Blocca"
- "Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti i loro messaggi saranno nascosti. Potrai annullare questa azione in qualsiasi momento."
+ "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."
"Blocca utente"
"Sblocca"
"Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi."
"Sblocca utente"
- "I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli."
- "Crittografia messaggi abilitata"
- "Invita persone"
"Esci dalla stanza"
"Persone"
"Sicurezza"
- "Condividi stanza"
"Oggetto"
\ No newline at end of file
diff --git a/features/roomdetails/impl/src/main/res/values-ro/translations.xml b/features/roomdetails/impl/src/main/res/values-ro/translations.xml
index db6777fb7f..3525b06d8e 100644
--- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml
@@ -5,18 +5,18 @@
- "%1$d persoane"
+ "Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca."
+ "Criptarea mesajelor este activată"
+ "Invitați persoane"
+ "Partajați camera"
"Blocați"
"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."
"Blocați utilizatorul"
"Deblocați"
"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."
"Deblocați utilizatorul"
- "Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca."
- "Criptarea mesajelor este activată"
- "Invitați persoane"
"Părăsiți camera"
"Persoane"
"Securitate"
- "Partajați camera"
"Subiect"
\ No newline at end of file
diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml
index f63757a8e3..584f4322d3 100644
--- a/features/roomdetails/impl/src/main/res/values/localazy.xml
+++ b/features/roomdetails/impl/src/main/res/values/localazy.xml
@@ -4,18 +4,18 @@
- "1 person"
- "%1$d people"
+ "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."
+ "Message encryption enabled"
+ "Invite people"
+ "Share room"
"Block"
"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."
"Block user"
"Unblock"
"On unblocking the user, you will be able to see all messages by them again."
"Unblock user"
- "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."
- "Message encryption enabled"
- "Invite people"
"Leave room"
"People"
"Security"
- "Share room"
"Topic"
\ No newline at end of file
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt
index 66e74cd5cc..2cc6d0ec24 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt
@@ -23,9 +23,14 @@ 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.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.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomMembershipState
@@ -34,7 +39,10 @@ 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.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
+import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
@@ -46,11 +54,21 @@ import org.junit.Test
class RoomDetailsPresenterTests {
private val roomMembershipObserver = RoomMembershipObserver()
+ private val testCoroutineDispatchers = testCoroutineDispatchers()
+
+ private fun aRoomDetailsPresenter(room: MatrixRoom): RoomDetailsPresenter {
+ val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
+ override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
+ return RoomMemberDetailsPresenter(aMatrixClient(), room, roomMemberId)
+ }
+ }
+ return RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers, roomMemberDetailsPresenterFactory)
+ }
@Test
fun `present - initial state is created from room info`() = runTest {
val room = aMatrixRoom()
- val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
+ val presenter = aRoomDetailsPresenter(room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
@@ -59,7 +77,7 @@ class RoomDetailsPresenterTests {
Truth.assertThat(initialState.roomName).isEqualTo(room.name)
Truth.assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl)
Truth.assertThat(initialState.roomTopic).isEqualTo(room.topic)
- Truth.assertThat(initialState.memberCount).isEqualTo(Async.Loading(null))
+ Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized)
Truth.assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted)
cancelAndIgnoreRemainingEvents()
@@ -68,23 +86,42 @@ class RoomDetailsPresenterTests {
@Test
fun `present - room member count is calculated asynchronously`() = runTest {
+ val error = RuntimeException()
val room = aMatrixRoom()
- val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
+ val roomMembers = listOf(
+ aRoomMember(A_USER_ID),
+ aRoomMember(A_USER_ID_2),
+ )
+ val presenter = aRoomDetailsPresenter(room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
+ room.givenRoomMembersState(MatrixRoomMembersState.Unknown)
val initialState = awaitItem()
- Truth.assertThat(initialState.memberCount).isEqualTo(Async.Loading(null))
+ Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized)
- val finalState = awaitItem()
- Truth.assertThat(finalState.memberCount).isEqualTo(Async.Success(0))
+ room.givenRoomMembersState(MatrixRoomMembersState.Pending(null))
+ val loadingState = awaitItem()
+ Truth.assertThat(loadingState.memberCount).isEqualTo(Async.Loading(null))
+
+ room.givenRoomMembersState(MatrixRoomMembersState.Error(error))
+ //skipItems(1)
+ val failureState = awaitItem()
+ Truth.assertThat(failureState.memberCount).isEqualTo(Async.Failure(error, null))
+
+ room.givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
+ //skipItems(1)
+ val successState = awaitItem()
+ Truth.assertThat(successState.memberCount).isEqualTo(Async.Success(roomMembers.size))
+
+ cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - initial state with no room name`() = runTest {
val room = aMatrixRoom(name = null)
- val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
+ val presenter = aRoomDetailsPresenter(room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
@@ -96,16 +133,22 @@ class RoomDetailsPresenterTests {
}
@Test
- fun `present - can handle error while fetching member count`() = runTest {
- val room = aMatrixRoom(name = null).apply {
- givenFetchMemberResult(Result.failure(Throwable()))
+ fun `present - initial state with DM member sets custom DM roomType`() = runTest {
+ val myRoomMember = aRoomMember(A_SESSION_ID)
+ val otherRoomMember = aRoomMember(A_USER_ID_2)
+ val room = aMatrixRoom(
+ isEncrypted = true,
+ isDirect = true,
+ ).apply {
+ val roomMembers = listOf(myRoomMember, otherRoomMember)
+ givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
}
- val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
+ val presenter = aRoomDetailsPresenter(room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
- skipItems(1)
- Truth.assertThat(awaitItem().memberCount).isInstanceOf(Async.Failure::class.java)
+ val initialState = awaitItem()
+ Truth.assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember))
cancelAndIgnoreRemainingEvents()
}
@@ -113,15 +156,14 @@ class RoomDetailsPresenterTests {
@Test
fun `present - Leave with confirmation on private room shows a specific warning`() = runTest {
- val room = aMatrixRoom(isPublic = false)
- val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
+ val room = aMatrixRoom(isPublic = false).apply {
+ givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
+ }
+ val presenter = aRoomDetailsPresenter(room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
- // Allow room member count to load
- skipItems(1)
-
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
val confirmationState = awaitItem()
Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.PrivateRoom)
@@ -130,15 +172,14 @@ class RoomDetailsPresenterTests {
@Test
fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest {
- val room = aMatrixRoom(members = listOf(aRoomMember()))
- val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
+ val room = aMatrixRoom().apply {
+ givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(aRoomMember())))
+ }
+ val presenter = aRoomDetailsPresenter(room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
- // Allow room member count to load
- skipItems(1)
-
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
val confirmationState = awaitItem()
Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.LastUserInRoom)
@@ -147,15 +188,14 @@ class RoomDetailsPresenterTests {
@Test
fun `present - Leave with confirmation shows a generic warning`() = runTest {
- val room = aMatrixRoom()
- val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
+ val room = aMatrixRoom().apply {
+ givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
+ }
+ val presenter = aRoomDetailsPresenter(room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
- // Allow room member count to load
- skipItems(1)
-
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
val confirmationState = awaitItem()
Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.Generic)
@@ -164,15 +204,14 @@ class RoomDetailsPresenterTests {
@Test
fun `present - Leave without confirmation leaves the room`() = runTest {
- val room = aMatrixRoom()
- val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
+ val room = aMatrixRoom().apply {
+ givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
+ }
+ val presenter = aRoomDetailsPresenter(room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
- // Allow room member count to load
- skipItems(1)
-
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false))
cancelAndIgnoreRemainingEvents()
@@ -189,14 +228,11 @@ class RoomDetailsPresenterTests {
val room = aMatrixRoom().apply {
givenLeaveRoomError(Throwable())
}
- val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
+ val presenter = aRoomDetailsPresenter(room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
- // Allow room member count to load
- skipItems(1)
-
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false))
val errorState = awaitItem()
Truth.assertThat(errorState.error).isNotNull()
@@ -206,24 +242,28 @@ class RoomDetailsPresenterTests {
}
}
+fun aMatrixClient(
+ sessionId: SessionId = A_SESSION_ID,
+) = FakeMatrixClient()
+
fun aMatrixRoom(
roomId: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME,
displayName: String = "A fallback display name",
topic: String? = "A topic",
avatarUrl: String? = "https://matrix.org/avatar.jpg",
- members: List = emptyList(),
isEncrypted: Boolean = true,
isPublic: Boolean = true,
+ isDirect: Boolean = false,
) = FakeMatrixRoom(
roomId = roomId,
name = name,
displayName = displayName,
topic = topic,
avatarUrl = avatarUrl,
- members = members,
isEncrypted = isEncrypted,
isPublic = isPublic,
+ isDirect = isDirect,
)
fun aRoomMember(
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt
index 373ebbb347..a5798381c0 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt
@@ -29,8 +29,12 @@ import io.element.android.features.userlist.api.UserListPresenterArgs
import io.element.android.features.userlist.impl.DefaultUserListPresenter
import io.element.android.features.userlist.test.FakeUserListDataSource
import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.ui.components.aMatrixUser
+import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import okhttp3.internal.toImmutableList
import org.junit.Test
@@ -38,6 +42,8 @@ import org.junit.Test
@ExperimentalCoroutinesApi
class RoomMemberListPresenterTests {
+ private val testCoroutineDispatchers = testCoroutineDispatchers()
+
@Test
fun `present - search is done automatically on start, but is async`() = runTest {
val searchResult = listOf(aMatrixUser())
@@ -52,7 +58,14 @@ class RoomMemberListPresenterTests {
userListDataStore: UserListDataStore,
) = DefaultUserListPresenter(args, userListDataSource, userListDataStore)
}
- val presenter = RoomMemberListPresenter(userListFactory, userListDataSource, userListDataStore)
+ val fakeRoom = FakeMatrixRoom()
+ val presenter = RoomMemberListPresenter(
+ userListPresenterFactory = userListFactory,
+ userListDataSource = userListDataSource,
+ userListDataStore = userListDataStore,
+ room = fakeRoom,
+ coroutineDispatchers = testCoroutineDispatchers
+ )
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt
index 68de573fae..13eb28ca85 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt
@@ -20,9 +20,13 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth
+import io.element.android.features.roomdetails.aMatrixClient
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.api.room.MatrixRoomMembersState
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -30,14 +34,17 @@ import org.junit.Test
@ExperimentalCoroutinesApi
class RoomMemberDetailsPresenterTests {
+ private val matrixClient = aMatrixClient()
+
@Test
fun `present - returns the room member's data, then updates it if needed`() = runTest {
+ val roomMember = aRoomMember(displayName = "Alice")
val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.success("A custom name"))
givenUserAvatarUrlResult(Result.success("A custom avatar"))
+ givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember)))
}
- val roomMember = aRoomMember(displayName = "Alice")
- val presenter = RoomMemberDetailsPresenter(room, roomMember)
+ val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
@@ -55,12 +62,13 @@ class RoomMemberDetailsPresenterTests {
@Test
fun `present - will recover when retrieving room member details fails`() = runTest {
+ val roomMember = aRoomMember(displayName = "Alice")
val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.failure(Throwable()))
givenUserAvatarUrlResult(Result.failure(Throwable()))
+ givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember)))
}
- val roomMember = aRoomMember(displayName = "Alice")
- val presenter = RoomMemberDetailsPresenter(room, roomMember)
+ val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
@@ -74,12 +82,13 @@ class RoomMemberDetailsPresenterTests {
@Test
fun `present - will fallback to original data if the updated data is null`() = runTest {
+ val roomMember = aRoomMember(displayName = "Alice")
val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.success(null))
givenUserAvatarUrlResult(Result.success(null))
+ givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember)))
}
- val roomMember = aRoomMember(displayName = "Alice")
- val presenter = RoomMemberDetailsPresenter(room, roomMember)
+ val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
@@ -90,4 +99,63 @@ class RoomMemberDetailsPresenterTests {
ensureAllEventsConsumed()
}
}
+
+ @Test
+ fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest {
+ val room = aMatrixRoom()
+ val roomMember = aRoomMember()
+ val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId)
+ 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(matrixClient, room, roomMember.userId)
+ 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(matrixClient, room, roomMember.userId)
+ 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()
+ }
+ }
}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
index 299c670eb4..684342bee8 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
@@ -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
}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
index 38c80734e2..f366c25326 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
@@ -79,6 +79,7 @@ class RoomListPresenter @Inject constructor(
Timber.v("RoomSummaries size = ${roomSummaries.size}")
+ val mappedRoomSummaries: MutableState> = remember { mutableStateOf(persistentListOf()) }
val filteredRoomSummaries: MutableState> = 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?, filter: String): ImmutableList {
- 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, filter: String): ImmutableList {
+ return when {
+ filter.isEmpty() -> emptyList()
+ else -> mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) }
}.toImmutableList()
}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
index 185c7c94a9..6fd629d67f 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
@@ -26,10 +26,12 @@ import kotlinx.collections.immutable.ImmutableList
data class RoomListState(
val matrixUser: MatrixUser?,
val roomList: ImmutableList,
- val filter: String,
+ val filter: String?,
+ val filteredRoomList: ImmutableList,
val displayVerificationPrompt: Boolean,
val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?,
val displayInvites: Boolean,
+ val displaySearchResults: Boolean,
val eventSink: (RoomListEvents) -> Unit
)
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
index 1dfa943660..a95456b6a4 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
@@ -36,6 +36,8 @@ open class RoomListStateProvider : PreviewParameterProvider {
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 = {}
)
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
index 8c591f2c2b..cd42548007 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
@@ -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 = {})
+ }
+}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt
index 1d1a4e7c10..11f0616e07 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt
@@ -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(
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt
new file mode 100644
index 0000000000..c2baca9a6b
--- /dev/null
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt
@@ -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)
+ }
+ }
+ }
+ }
+}
diff --git a/features/roomlist/impl/src/main/res/values-de/translations.xml b/features/roomlist/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..00b1431f00
--- /dev/null
+++ b/features/roomlist/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,40 @@
+
+
+ "Alle Chats"
+ "(Avatar wurde ebenfalls geändert)"
+ "%1$s hat seinen Avatar geändert"
+ "Du hast deinen Avatar geändert"
+ "%1$s hat den Anzeigenamen von %2$s in %3$s geändert"
+ "Du hast deinen Anzeigenamen von %1$s in %2$s geändert"
+ "%1$s hat den Anzeigenamen entfernt (war %2$s)"
+ "Du hast deinen Anzeigenamen entfernt (war %1$s)"
+ "%1$s hat den Anzeigenamen auf %2$s gesetzt"
+ "Du hast deinen Anzeigenamen auf %1$s gesetzt"
+ "%1$s hat den Raum-Avatar geändert"
+ "Du hast den Raum-Avatar geändert"
+ "%1$s hat den Raum-Avatar entfernt"
+ "%1$s hat den Raum erstellt"
+ "Du hast den Raum erstellt"
+ "%1$s hat %2$s eingeladen"
+ "%1$s hat die Einladung angenommen"
+ "Du hast die Einladung angenommen"
+ "Du hast %1$s eingeladen"
+ "%1$s hat dich eingeladen"
+ "%1$s ist dem Raum beigetreten"
+ "Du bist dem Raum beigetreten"
+ "%1$s hat deine Beitrittsanfrage abgelehnt"
+ "%1$s hat den Raum verlassen"
+ "Du hast den Raum verlassen"
+ "%1$s hat den Raumnamen geändert in: %2$s"
+ "Sie haben den Raumnamen geändert in: %1$s"
+ "%1$s hat den Raumnamen entfernt"
+ "Du hast den Raumnamen entfernt"
+ "%1$s hat die Einladung abgelehnt"
+ "Du hast die Einladung abgelehnt"
+ "%1$s hat %2$s entfernt"
+ "Du hast %1$s entfernt"
+ "%1$s hat das Thema geändert zu: %2$s"
+ "Sie haben das Thema geändert zu: %1$s"
+ "%1$s hat das Raumthema entfernt"
+ "Du hast das Raumthema entfernt"
+
\ No newline at end of file
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
index e28eb1e362..5dd73980f8 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
@@ -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()
}
}
diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt
index afe2d1ab3d..2cfd23eb61 100644
--- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt
+++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt
@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.model.MatrixUser
interface UserListDataSource {
+ //TODO should probably have a flow
suspend fun search(query: String): List
suspend fun getProfile(userId: UserId): MatrixUser?
}
diff --git a/features/verifysession/impl/src/main/res/values-de/translations.xml b/features/verifysession/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..e3817c2507
--- /dev/null
+++ b/features/verifysession/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,10 @@
+
+
+ "Emojis vergleichen"
+ "Beweise, dass du es bist, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen."
+ "Ich bin bereit"
+ "Warten auf Übereinstimmung"
+ "Sie stimmen nicht überein"
+ "Sie stimmen überein"
+ "Verifizierung abgebrochen"
+
\ No newline at end of file
diff --git a/features/verifysession/impl/src/main/res/values-es/translations.xml b/features/verifysession/impl/src/main/res/values-es/translations.xml
index 839c945e24..ccc656e845 100644
--- a/features/verifysession/impl/src/main/res/values-es/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-es/translations.xml
@@ -1,7 +1,6 @@
"Algo no fue bien. Se agotó el tiempo de espera de la solicitud o se rechazó."
- "Verificación cancelada"
"Confirma que los emojis que aparecen a continuación coinciden con los que aparecen en tu otra sesión."
"Comparar emojis"
"Tu nueva sesión ya está verificada. Tienes acceso a tus mensajes cifrados y otros usuarios lo considerarán de confianza."
@@ -9,11 +8,12 @@
"Abrir una sesión existente"
"Reintentar la verificación"
"Estoy listo"
- "Comenzar"
"Esperando a que coincida"
"Compara los emoji, asegurándote de que aparecen en el mismo orden."
"No coinciden"
"Coinciden"
"Acepta la solicitud para iniciar el proceso de verificación en tu otra sesión para continuar."
"A la espera de aceptar la solicitud"
+ "Verificación cancelada"
+ "Comenzar"
\ No newline at end of file
diff --git a/features/verifysession/impl/src/main/res/values-it/translations.xml b/features/verifysession/impl/src/main/res/values-it/translations.xml
index 3d8a46d581..1bf0e87ea9 100644
--- a/features/verifysession/impl/src/main/res/values-it/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-it/translations.xml
@@ -1,7 +1,6 @@
"C\'è qualcosa che non va. La richiesta è scaduta o è stata rifiutata."
- "Verifica annullata"
"Verifica che gli emoji sottostanti corrispondano a quelli mostrati nell\'altra sessione."
"Confronta le emoji"
"La tua nuova sessione è ora verificata. Ha accesso ai tuoi messaggi crittografati e gli altri utenti la vedranno come attendibile."
@@ -9,11 +8,12 @@
"Apri una sessione esistente"
"Riprova la verifica"
"Sono pronto"
- "Inizia"
"In attesa di un riscontro"
"Confronta le emoji uniche, assicurandoti che appaiano nello stesso ordine."
"Non corrispondono"
"Corrispondono"
"Accetta la richiesta di avviare il processo di verifica nell\'altra sessione per continuare."
"In attesa di accettare la richiesta"
+ "Verifica annullata"
+ "Inizia"
\ No newline at end of file
diff --git a/features/verifysession/impl/src/main/res/values-ro/translations.xml b/features/verifysession/impl/src/main/res/values-ro/translations.xml
index f2bade56fc..3ad0de6e56 100644
--- a/features/verifysession/impl/src/main/res/values-ro/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-ro/translations.xml
@@ -1,7 +1,6 @@
"Ceva nu este în regulă. Fie cererea a expirat, fie a fost respinsă."
- "Verificare anulată"
"Confirmați că emoticoanele de mai jos se potrivesc cu cele afișate în cealaltă sesiune."
"Comparați emoticoanele"
"Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar alți utilizatori vă vor vedea ca fiind de încredere."
@@ -9,11 +8,12 @@
"Deschideți o sesiune existentă"
"Reîncercați verificarea"
"Sunt pregătit"
- "Începeți"
"Se așteaptă confirmarea"
"Comparăți emoticoalene asigurându-vă că apar în aceeași ordine."
"Nu se potrivesc"
"Se potrivesc"
"Acceptați solicitarea de a începe procesul de verificare în cealaltă sesiune pentru a continua."
"Se așteptă acceptarea cererii"
+ "Verificare anulată"
+ "Începeți"
\ No newline at end of file
diff --git a/features/verifysession/impl/src/main/res/values/localazy.xml b/features/verifysession/impl/src/main/res/values/localazy.xml
index fd81d104fb..c217f0d2a4 100644
--- a/features/verifysession/impl/src/main/res/values/localazy.xml
+++ b/features/verifysession/impl/src/main/res/values/localazy.xml
@@ -1,7 +1,6 @@
"Something doesn’t seem right. Either the request timed out or the request was denied."
- "Verification cancelled"
"Confirm that the emojis below match those shown on your other session."
"Compare emojis"
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted."
@@ -9,11 +8,12 @@
"Open an existing session"
"Retry verification"
"I am ready"
- "Start"
"Waiting to match"
"Compare the unique emoji, ensuring they appear in the same order."
"They don’t match"
"They match"
"Accept the request to start the verification process in your other session to continue."
"Waiting to accept request"
+ "Verification cancelled"
+ "Start"
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 275145a865..cd16d4f022 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,7 +3,7 @@
[versions]
# Project
-android_gradle_plugin = "8.0.0"
+android_gradle_plugin = "8.0.1"
kotlin = "1.8.20"
ksp = "1.8.20-1.0.11"
molecule = "0.9.0"
@@ -36,14 +36,14 @@ coil = "2.3.0"
datetime = "0.4.0"
serialization_json = "1.5.0"
showkase = "1.0.0-beta17"
-jsoup = "1.15.4"
+jsoup = "1.16.1"
appyx = "1.2.0"
dependencycheck = "8.2.1"
stem = "2.3.0"
sqldelight = "1.5.5"
# DI
-dagger = "2.45"
+dagger = "2.46"
anvil = "2.4.5"
# quality
@@ -55,7 +55,7 @@ dependencygraph = "0.10"
android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" }
kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
# https://firebase.google.com/docs/android/setup#available-libraries
-google_firebase_bom = "com.google.firebase:firebase-bom:31.5.0"
+google_firebase_bom = "com.google.firebase:firebase-bom:32.0.0"
# AndroidX
androidx_material = { module = "com.google.android.material:material", version.ref = "material" }
@@ -128,11 +128,11 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1"
-matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.10"
+matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.11"
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
-sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.3"
+sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
sqlite = "androidx.sqlite:sqlite:2.3.1"
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
gujun_span = "me.gujun.android:span:1.7"
@@ -164,7 +164,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
anvil = { id = "com.squareup.anvil", version.ref = "anvil" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
-ktlint = "org.jlleitschuh.gradle.ktlint:11.3.1"
+ktlint = "org.jlleitschuh.gradle.ktlint:11.3.2"
dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" }
dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" }
paparazzi = "app.cash.paparazzi:1.2.0"
diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt
index bb74dff2a9..3be961598d 100644
--- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt
+++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt
@@ -47,7 +47,9 @@ suspend fun (suspend () -> T).execute(state: MutableState>, errorMa
}
suspend fun (suspend () -> Result).executeResult(state: MutableState>) {
- state.value = Async.Loading()
+ if (state.value !is Async.Success) {
+ state.value = Async.Loading()
+ }
this().fold(
onSuccess = {
state.value = Async.Success(it)
diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/ErrorFlow.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/ErrorFlow.kt
new file mode 100644
index 0000000000..302978066c
--- /dev/null
+++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/ErrorFlow.kt
@@ -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 errorFlow(throwable: Throwable) = flow { throw throwable }
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt
index 06aa4435a0..e5dcc0c5ff 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt
@@ -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,11 +95,11 @@ private fun InitialsAvatar(
end = Offset(100f, 0f)
)
Box(
- modifier.background(brush = initialsGradient)
+ modifier.background(brush = initialsGradient),
) {
Text(
modifier = Modifier.align(Alignment.Center),
- text = avatarData.getInitial(),
+ text = avatarData.initial,
fontSize = (avatarData.size.dp / 2).value.sp,
color = Color.White,
)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt
index 3bf4f7d0b4..2d1e0558f4 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt
@@ -30,8 +30,37 @@ data class AvatarData(
@IgnoredOnParcel
val size: AvatarSize = AvatarSize.MEDIUM
) : Parcelable {
- fun getInitial(): String {
- val firstChar = name?.firstOrNull() ?: id.getOrNull(1) ?: '?'
- return firstChar.uppercase()
+
+ @IgnoredOnParcel
+ val initial by lazy {
+ (name?.takeIf { it.isNotBlank() } ?: id)
+ .let { dn ->
+ var startIndex = 0
+ val initial = dn[startIndex]
+
+ if (initial in listOf('@', '#', '+') && dn.length > 1) {
+ startIndex++
+ }
+
+ var length = 1
+ var first = dn[startIndex]
+
+ // LEFT-TO-RIGHT MARK
+ if (dn.length >= 2 && 0x200e == first.code) {
+ startIndex++
+ first = dn[startIndex]
+ }
+
+ // check if it’s the start of a surrogate pair
+ if (first.code in 0xD800..0xDBFF && dn.length > startIndex + 1) {
+ val second = dn[startIndex + 1]
+ if (second.code in 0xDC00..0xDFFF) {
+ length++
+ }
+ }
+
+ dn.substring(startIndex, startIndex + length)
+ }
+ .uppercase()
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/ApplyIf.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/ApplyIf.kt
new file mode 100644
index 0000000000..a18d0ef3ed
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/ApplyIf.kt
@@ -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
+ }
+ }
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt
new file mode 100644
index 0000000000..9675c54a20
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt
@@ -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,
+ 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)
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt
index 7c498a610c..d897f6ad4e 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt
@@ -21,12 +21,12 @@ package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.MaterialTheme
import androidx.compose.material.ModalBottomSheetDefaults
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
-import androidx.compose.material.contentColorFor
import androidx.compose.material.rememberModalBottomSheetState
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -44,7 +44,7 @@ fun ModalBottomSheetLayout(
sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden),
sheetShape: Shape = MaterialTheme.shapes.large,
sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
- sheetBackgroundColor: Color = MaterialTheme.colors.surface,
+ sheetBackgroundColor: Color = MaterialTheme.colorScheme.surface,
sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
content: @Composable () -> Unit = {}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt
new file mode 100644
index 0000000000..33baf19dce
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt
@@ -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)
+ )
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
index 0427c147de..f0bd7d62a4 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
@@ -36,6 +36,8 @@ interface MatrixClient : Closeable {
val mediaLoader: MatrixMediaLoader
fun getRoom(roomId: RoomId): MatrixRoom?
fun findDM(userId: UserId): MatrixRoom?
+ suspend fun ignoreUser(userId: UserId): Result
+ suspend fun unignoreUser(userId: UserId): Result
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result
suspend fun createDM(userId: UserId): Result
fun startSync()
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
index 27fc15c2c6..991f8dd117 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
@@ -16,12 +16,19 @@
package io.element.android.libraries.matrix.api.notification
-import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.UserId
+//TODO add content
data class NotificationData(
- val item: MatrixTimelineItem,
- val title: String,
- val subtitle: String?,
+ val senderId: UserId,
+ val eventId: EventId,
+ val roomId: RoomId,
+ val senderAvatarUrl: String? = null,
+ val senderDisplayName: String? = null,
+ val roomAvatarUrl: String? = null,
+ val isDirect: Boolean,
+ val isEncrypted: Boolean,
val isNoisy: Boolean,
- val avatarUrl: String?,
)
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
index 70980cc753..35451874aa 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
@@ -18,12 +18,15 @@ package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
+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.timeline.MatrixTimeline
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
import java.io.Closeable
interface MatrixRoom : Closeable {
+ val sessionId: SessionId
val roomId: RoomId
val name: String?
val bestName: String
@@ -36,20 +39,22 @@ interface MatrixRoom : Closeable {
val isDirect: Boolean
val isPublic: Boolean
- suspend fun members(): List
+ /**
+ * The current loaded members as a StateFlow.
+ * Initial value is [MatrixRoomMembersState.Unknown].
+ * To update them you should call [updateMembers].
+ */
+ val membersStateFlow: StateFlow
- suspend fun memberCount(): Int
-
- fun getMember(userId: UserId): RoomMember?
-
- fun getDmMember(): RoomMember?
+ /**
+ * Try to load the room members and update the membersFlow.
+ */
+ suspend fun updateMembers(): Result
fun syncUpdateFlow(): Flow
fun timeline(): MatrixTimeline
- suspend fun fetchMembers(): Result
-
suspend fun userDisplayName(userId: UserId): Result
suspend fun userAvatarUrl(userId: UserId): Result
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt
new file mode 100644
index 0000000000..4e41fd43ba
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.api.room
+
+sealed interface MatrixRoomMembersState {
+ object Unknown : MatrixRoomMembersState
+ data class Pending(val prevRoomMembers: List? = null) : MatrixRoomMembersState
+ data class Error(val failure: Throwable, val prevRoomMembers: List? = null) : MatrixRoomMembersState
+ data class Ready(val roomMembers: List) : MatrixRoomMembersState
+}
+
+fun MatrixRoomMembersState.roomMembers(): List? {
+ return when (this) {
+ is MatrixRoomMembersState.Ready -> roomMembers
+ is MatrixRoomMembersState.Pending -> prevRoomMembers
+ is MatrixRoomMembersState.Error -> prevRoomMembers
+ else -> null
+ }
+}
+
+
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt
index 25bb3e8a7e..3c9bd030b0 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt
@@ -16,11 +16,8 @@
package io.element.android.libraries.matrix.api.room
-import android.os.Parcelable
import io.element.android.libraries.matrix.api.core.UserId
-import kotlinx.parcelize.Parcelize
-@Parcelize
data class RoomMember(
val userId: UserId,
val displayName: String?,
@@ -30,7 +27,7 @@ data class RoomMember(
val powerLevel: Long,
val normalizedPowerLevel: Long,
val isIgnored: Boolean,
-) : Parcelable
+)
enum class RoomMembershipState {
BAN, INVITE, JOIN, KNOCK, LEAVE
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
index e6f6068c0d..bef0bc64c7 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
@@ -200,7 +200,7 @@ class RustMatrixClient constructor(
val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null
val fullRoom = slidingSyncRoom.fullRoom() ?: return null
return RustMatrixRoom(
- currentUserId = sessionId,
+ sessionId = sessionId,
slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow,
slidingSyncRoom = slidingSyncRoom,
innerRoom = fullRoom,
@@ -214,6 +214,18 @@ class RustMatrixClient constructor(
return roomId?.let { getRoom(it) }
}
+ override suspend fun ignoreUser(userId: UserId): Result = withContext(dispatchers.io) {
+ runCatching {
+ client.ignoreUser(userId.value)
+ }
+ }
+
+ override suspend fun unignoreUser(userId: UserId): Result = withContext(dispatchers.io) {
+ runCatching {
+ client.unignoreUser(userId.value)
+ }
+ }
+
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result = withContext(dispatchers.io) {
runCatching {
val rustParams = RustCreateRoomParameters(
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt
index d4b8f4f72e..6d7d48db24 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt
@@ -63,7 +63,7 @@ class RustMediaLoader(
runCatching {
mediaSourceFromUrl(url).use { mediaSource ->
innerClient.getMediaFile(
- source = mediaSource,
+ mediaSource = mediaSource,
mimeType = mimeType ?: "application/octet-stream"
).use {
Path(it.path())
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt
index ae47beb700..079b1e0a5a 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt
@@ -16,35 +16,28 @@
package io.element.android.libraries.matrix.impl.notification
+import io.element.android.libraries.matrix.api.core.EventId
+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.notification.NotificationData
-import io.element.android.libraries.matrix.impl.timeline.MatrixTimelineItemMapper
-import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
-import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
-import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
-import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
import org.matrix.rustcomponents.sdk.NotificationItem
import org.matrix.rustcomponents.sdk.use
import javax.inject.Inject
class NotificationMapper @Inject constructor() {
- // TODO Inject and remove duplicate?
- private val timelineItemFactory = MatrixTimelineItemMapper(
- virtualTimelineItemMapper = VirtualTimelineItemMapper(),
- eventTimelineItemMapper = EventTimelineItemMapper(
- contentMapper = TimelineEventContentMapper(
- eventMessageMapper = EventMessageMapper()
- )
- )
- )
fun map(notificationItem: NotificationItem): NotificationData {
return notificationItem.use {
NotificationData(
- item = timelineItemFactory.map(it.item),
- title = it.title,
- subtitle = it.subtitle,
- isNoisy = it.isNoisy,
- avatarUrl = it.avatarUrl,
+ senderId = UserId(it.event.senderId()),
+ eventId = EventId(it.event.eventId()),
+ roomId = RoomId(it.roomId),
+ senderAvatarUrl = it.senderAvatarUrl,
+ senderDisplayName = it.senderDisplayName,
+ roomAvatarUrl = it.roomAvatarUrl,
+ isDirect = it.isDirect,
+ isEncrypted = it.isEncrypted,
+ isNoisy = it.isNoisy
)
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
index 8afa3cb4ed..9caba227ce 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
@@ -17,21 +17,23 @@
package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
-import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
+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 io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
+import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
@@ -40,7 +42,7 @@ import org.matrix.rustcomponents.sdk.genTransactionId
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
class RustMatrixRoom(
- private val currentUserId: UserId,
+ override val sessionId: SessionId,
private val slidingSyncUpdateFlow: Flow,
private val slidingSyncRoom: SlidingSyncRoom,
private val innerRoom: Room,
@@ -48,38 +50,19 @@ class RustMatrixRoom(
private val coroutineDispatchers: CoroutineDispatchers,
) : MatrixRoom {
- private var loadMembersJob: Job? = null
- private var cachedMembers: List = emptyList()
+ override val membersStateFlow: StateFlow
+ get() = _membersStateFlow
- override suspend fun members(): List {
- return cachedMembers.ifEmpty {
- if (loadMembersJob == null) {
- loadMembersJob = coroutineScope.launch(coroutineDispatchers.io) {
- cachedMembers = tryOrNull {
- innerRoom.members().map(RoomMemberMapper::map)
- } ?: emptyList()
- }
- }
- loadMembersJob?.join()
- loadMembersJob = null
- cachedMembers
- }
- }
+ private var _membersStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown)
- override suspend fun memberCount(): Int {
- return members().size
- }
-
- override fun getMember(userId: UserId): RoomMember? {
- return cachedMembers.find { it.userId == userId }
- }
-
- override fun getDmMember(): RoomMember? {
- return if (cachedMembers.size == 2 && isDirect && isEncrypted) {
- cachedMembers.find { it.userId != currentUserId }
- } else {
- null
- }
+ private val timeline by lazy {
+ RustMatrixTimeline(
+ matrixRoom = this,
+ innerRoom = innerRoom,
+ slidingSyncRoom = slidingSyncRoom,
+ coroutineScope = coroutineScope,
+ coroutineDispatchers = coroutineDispatchers
+ )
}
override fun syncUpdateFlow(): Flow {
@@ -94,13 +77,7 @@ class RustMatrixRoom(
}
override fun timeline(): MatrixTimeline {
- return RustMatrixTimeline(
- matrixRoom = this,
- innerRoom = innerRoom,
- slidingSyncRoom = slidingSyncRoom,
- coroutineScope = coroutineScope,
- coroutineDispatchers = coroutineDispatchers
- )
+ return timeline
}
override fun close() {
@@ -150,9 +127,16 @@ class RustMatrixRoom(
override val isDirect: Boolean
get() = innerRoom.isDirect()
- override suspend fun fetchMembers(): Result = withContext(coroutineDispatchers.io) {
+ override suspend fun updateMembers(): Result = withContext(coroutineDispatchers.io) {
+ val currentState = _membersStateFlow.value
+ val currentMembers = currentState.roomMembers()
+ _membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers)
runCatching {
- innerRoom.fetchMembers()
+ innerRoom.members().map(RoomMemberMapper::map)
+ }.map {
+ _membersStateFlow.value = MatrixRoomMembersState.Ready(it)
+ }.onFailure {
+ _membersStateFlow.value = MatrixRoomMembersState.Error(prevRoomMembers = currentMembers, failure = it)
}
}
@@ -208,13 +192,13 @@ class RustMatrixRoom(
}
override suspend fun acceptInvitation(): Result = withContext(coroutineDispatchers.io) {
- kotlin.runCatching {
+ runCatching {
innerRoom.acceptInvitation()
}
}
override suspend fun rejectInvitation(): Result = withContext(coroutineDispatchers.io) {
- kotlin.runCatching {
+ runCatching {
innerRoom.rejectInvitation()
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt
index e942f31b76..6f64db76ef 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt
@@ -42,6 +42,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncRoom
import org.matrix.rustcomponents.sdk.TimelineItem
import org.matrix.rustcomponents.sdk.TimelineListener
import timber.log.Timber
+import java.util.concurrent.atomic.AtomicBoolean
class RustMatrixTimeline(
private val matrixRoom: MatrixRoom,
@@ -51,6 +52,8 @@ class RustMatrixTimeline(
private val coroutineDispatchers: CoroutineDispatchers,
) : MatrixTimeline {
+ private val isInit = AtomicBoolean(false)
+
private val timelineItems: MutableStateFlow> =
MutableStateFlow(emptyList())
@@ -95,6 +98,7 @@ class RustMatrixTimeline(
withContext(coroutineDispatchers.diffUpdateDispatcher) {
this@RustMatrixTimeline.timelineItems.value = matrixTimelineItems
}
+ isInit.set(true)
}
.onFailure {
Timber.e("Failed adding timeline listener on room with identifier: ${matrixRoom.roomId})")
@@ -105,6 +109,7 @@ class RustMatrixTimeline(
override fun dispose() {
Timber.v("Dispose timeline for room ${matrixRoom.roomId}")
listenerTokens.dispose()
+ isInit.set(false)
}
/**
@@ -125,6 +130,9 @@ class RustMatrixTimeline(
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result = withContext(coroutineDispatchers.io) {
runCatching {
Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
+ if (!isInit.get()) {
+ throw IllegalStateException("Timeline is not init yet")
+ }
val paginationOptions = PaginationOptions.UntilNumItems(
eventLimit = requestSize.toUShort(),
items = untilNumberOfItems.toUShort()
@@ -139,10 +147,26 @@ class RustMatrixTimeline(
private suspend fun addListener(timelineListener: TimelineListener): Result> = withContext(coroutineDispatchers.io) {
runCatching {
- val settings = RoomSubscription(requiredState = listOf(RequiredState(key = "m.room.canonical_alias", value = "")), timelineLimit = null)
+ val settings = RoomSubscription(
+ requiredState = listOf(
+ RequiredState(key = "m.room.canonical_alias", value = ""),
+ RequiredState(key = "m.room.topic", value = ""),
+ RequiredState(key = "m.room.join_rules", value = ""),
+ ),
+ timelineLimit = null
+ )
val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings)
+ launch {
+ fetchMembers()
+ }
listenerTokens += result.taskHandle
result.items
}
}
+
+ private suspend fun fetchMembers() = withContext(coroutineDispatchers.io) {
+ runCatching {
+ innerRoom.fetchMembers()
+ }
+ }
}
diff --git a/libraries/matrix/test/build.gradle.kts b/libraries/matrix/test/build.gradle.kts
index 9f1d35112f..6d9ca1eb8e 100644
--- a/libraries/matrix/test/build.gradle.kts
+++ b/libraries/matrix/test/build.gradle.kts
@@ -23,6 +23,7 @@ android {
}
dependencies {
+ api(projects.libraries.core)
api(projects.libraries.matrix.api)
api(libs.coroutines.core)
}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
index 0167f2f846..95236d185f 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
@@ -48,6 +48,8 @@ class FakeMatrixClient(
private val notificationService: FakeNotificationService = FakeNotificationService(),
) : MatrixClient {
+ private var ignoreUserResult: Result = Result.success(Unit)
+ private var unignoreUserResult: Result = Result.success(Unit)
private var createRoomResult: Result = Result.success(A_ROOM_ID)
private var createDmResult: Result = Result.success(A_ROOM_ID)
private var createDmFailure: Throwable? = null
@@ -63,6 +65,14 @@ class FakeMatrixClient(
return findDmResult
}
+ override suspend fun ignoreUser(userId: UserId): Result {
+ return ignoreUserResult
+ }
+
+ override suspend fun unignoreUser(userId: UserId): Result {
+ return unignoreUserResult
+ }
+
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result {
delay(100)
return createRoomResult
@@ -119,6 +129,14 @@ class FakeMatrixClient(
createDmResult = result
}
+ fun givenIgnoreUserResult(result: Result) {
+ ignoreUserResult = result
+ }
+
+ fun givenUnignoreUserResult(result: Result) {
+ unignoreUserResult = result
+ }
+
fun givenCreateDmError(failure: Throwable?) {
createDmFailure = failure
}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
index 05890f9e4a..28d6525739 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
@@ -18,17 +18,21 @@ package io.element.android.libraries.matrix.test.room
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
+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 io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
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
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
class FakeMatrixRoom(
+ override val sessionId: SessionId = A_SESSION_ID,
override val roomId: RoomId = A_ROOM_ID,
override val name: String? = null,
override val bestName: String = "",
@@ -40,19 +44,16 @@ class FakeMatrixRoom(
override val alternativeAliases: List = emptyList(),
override val isPublic: Boolean = true,
override val isDirect: Boolean = false,
- private val members: List = emptyList(),
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
) : MatrixRoom {
+ private var ignoreResult: Result = Result.success(Unit)
+ private var unignoreResult: Result = Result.success(Unit)
private var userDisplayNameResult = Result.success(null)
private var userAvatarUrlResult = Result.success(null)
+ private var updateMembersResult: Result = Result.success(Unit)
private var acceptInviteResult = Result.success(Unit)
private var rejectInviteResult = Result.success(Unit)
- private var dmMember: RoomMember? = null
- private var fetchMemberResult: Result = Result.success(Unit)
-
- var areMembersFetched: Boolean = false
- private set
var isInviteAccepted: Boolean = false
private set
@@ -62,6 +63,12 @@ class FakeMatrixRoom(
private var leaveRoomError: Throwable? = null
+ override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown)
+
+ override suspend fun updateMembers(): Result {
+ return updateMembersResult
+ }
+
override fun syncUpdateFlow(): Flow {
return emptyFlow()
}
@@ -70,18 +77,6 @@ class FakeMatrixRoom(
return matrixTimeline
}
- override suspend fun fetchMembers(): Result {
- return fetchMemberResult.also { result ->
- if (result.isSuccess) {
- areMembersFetched = true
- }
- }
- }
-
- override fun getDmMember(): RoomMember? {
- return dmMember
- }
-
override suspend fun userDisplayName(userId: UserId): Result {
return userDisplayNameResult
}
@@ -90,22 +85,6 @@ class FakeMatrixRoom(
return userAvatarUrlResult
}
- override suspend fun members(): List {
- return members
- }
-
- override suspend fun memberCount(): Int {
- if (fetchMemberResult.isSuccess) {
- return members.count()
- } else {
- throw fetchMemberResult.exceptionOrNull()!!
- }
- }
-
- override fun getMember(userId: UserId): RoomMember? {
- return members.firstOrNull { it.userId == userId }
- }
-
override suspend fun sendMessage(message: String): Result {
delay(100)
return Result.success(Unit)
@@ -155,12 +134,12 @@ class FakeMatrixRoom(
this.leaveRoomError = throwable
}
- fun givenFetchMemberResult(result: Result) {
- fetchMemberResult = result
+ fun givenRoomMembersState(state: MatrixRoomMembersState) {
+ membersStateFlow.value = state
}
- fun givenDmMember(roomMember: RoomMember) {
- this.dmMember = roomMember
+ fun givenUpdateMembersResult(result: Result) {
+ updateMembersResult = result
}
fun givenUserDisplayNameResult(displayName: Result) {
@@ -179,4 +158,11 @@ class FakeMatrixRoom(
rejectInviteResult = result
}
+ fun givenIgnoreResult(result: Result) {
+ ignoreResult = result
+ }
+
+ fun givenUnIgnoreResult(result: Result) {
+ unignoreResult = result
+ }
}
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt
new file mode 100644
index 0000000000..061ce365eb
--- /dev/null
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.matrix.ui.room
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+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.roomMembers
+
+@Composable
+fun MatrixRoom.getRoomMember(userId: UserId): State {
+ val roomMembersState by membersStateFlow.collectAsState()
+ return getRoomMember(roomMembersState = roomMembersState, userId = userId)
+}
+
+@Composable
+fun getRoomMember(roomMembersState: MatrixRoomMembersState, userId: UserId): State {
+ val roomMembers = roomMembersState.roomMembers()
+ return remember(roomMembers) {
+ derivedStateOf {
+ roomMembers?.find {
+ it.userId == userId
+ }
+ }
+ }
+}
+
+@Composable
+fun MatrixRoom.getDirectRoomMember(): State {
+ val roomMembersState by membersStateFlow.collectAsState()
+ return getDirectRoomMember(roomMembersState = roomMembersState)
+}
+
+@Composable
+fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): State {
+ val roomMembers = roomMembersState.roomMembers()
+ return remember(roomMembers) {
+ derivedStateOf {
+ if (roomMembers == null) {
+ null
+ } else if (roomMembers.size == 2 && isDirect && isEncrypted) {
+ roomMembers.find { it.userId != this.sessionId }
+ } else {
+ null
+ }
+ }
+ }
+}
+
diff --git a/libraries/mediapickers/build.gradle.kts b/libraries/mediapickers/build.gradle.kts
new file mode 100644
index 0000000000..444244d2f0
--- /dev/null
+++ b/libraries/mediapickers/build.gradle.kts
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.libraries.mediapickers"
+
+ dependencies {
+ implementation(projects.libraries.uiStrings)
+ implementation(projects.libraries.core)
+ implementation(libs.inject)
+
+ testImplementation(libs.test.junit)
+ testImplementation(libs.coroutines.test)
+ testImplementation(libs.test.truth)
+ testImplementation(libs.test.robolectric)
+ }
+}
diff --git a/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerLauncher.kt b/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerLauncher.kt
new file mode 100644
index 0000000000..16f21a9683
--- /dev/null
+++ b/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerLauncher.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.mediapickers
+
+import androidx.activity.compose.ManagedActivityResultLauncher
+
+/**
+ * Wrapper around [ManagedActivityResultLauncher] to be used with media/file pickers.
+ */
+interface PickerLauncher {
+ /** Starts the activity result launcher with its default input. */
+ fun launch()
+
+ /** Starts the activity result launcher with a [customInput]. */
+ fun launch(customInput: Input)
+}
+
+class ComposePickerLauncher(
+ private val managedLauncher: ManagedActivityResultLauncher,
+ private val defaultRequest: Input,
+) : PickerLauncher {
+ override fun launch() {
+ managedLauncher.launch(defaultRequest)
+ }
+
+ override fun launch(customInput: Input) {
+ managedLauncher.launch(customInput)
+ }
+}
+
+/** Needed for screenshot tests. */
+class NoOpPickerLauncher(
+ private val onResult: () -> Unit,
+) : PickerLauncher {
+ override fun launch() = onResult()
+ override fun launch(customInput: Input) = onResult()
+}
diff --git a/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerProvider.kt b/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerProvider.kt
new file mode 100644
index 0000000000..720378aaca
--- /dev/null
+++ b/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerProvider.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.mediapickers
+
+import android.content.Context
+import android.net.Uri
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.core.content.FileProvider
+import io.element.android.libraries.core.mimetype.MimeTypes
+import java.io.File
+import java.util.UUID
+import javax.inject.Inject
+
+class PickerProvider constructor(private val isInTest: Boolean) {
+
+ @Inject
+ constructor(): this(false)
+
+ /**
+ * Remembers and returns a [PickerLauncher] for a certain media/file [type].
+ */
+ @Composable
+ internal fun rememberPickerLauncher(
+ type: PickerType,
+ onResult: (Output) -> Unit,
+ ): PickerLauncher {
+ return if (LocalInspectionMode.current || isInTest) {
+ NoOpPickerLauncher { }
+ } else {
+ val contract = type.getContract()
+ val managedLauncher = rememberLauncherForActivityResult(contract = contract, onResult = onResult)
+ remember(type) { ComposePickerLauncher(managedLauncher, type.getDefaultRequest()) }
+ }
+ }
+
+ /**
+ * Remembers and returns a [PickerLauncher] for a gallery item, either a picture or a video.
+ * [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
+ */
+ @Composable
+ fun registerGalleryPicker(onResult: (Uri?) -> Unit): PickerLauncher {
+ // Tests and UI preview can't handle Contexts, so we might as well disable the whole picker
+ return if (LocalInspectionMode.current || isInTest) {
+ NoOpPickerLauncher { onResult(null) }
+ } else {
+ rememberPickerLauncher(type = PickerType.ImageAndVideo) { uri -> onResult(uri) }
+ }
+ }
+
+ /**
+ * Remembers and returns a [PickerLauncher] for a file of a certain [mimeType] (any type of file, by default).
+ * [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
+ */
+ @Composable
+ fun registerFilePicker(mimeType: String = MimeTypes.Any, onResult: (Uri?) -> Unit): PickerLauncher {
+ // Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker
+ return if (LocalInspectionMode.current || isInTest) {
+ NoOpPickerLauncher { onResult(null) }
+ } else {
+ rememberPickerLauncher(type = PickerType.File(mimeType)) { uri -> onResult(uri) }
+ }
+ }
+
+ /**
+ * Remembers and returns a [PickerLauncher] for taking a photo with a camera app.
+ * @param [onResult] will be called with either the photo's [Uri] or `null` if nothing was selected.
+ * @param [deleteAfter] When it's `true`, the taken photo will be automatically removed after calling [onResult].
+ * It's `true` by default.
+ */
+ @Composable
+ fun registerCameraPhotoPicker(onResult: (Uri?) -> Unit, deleteAfter: Boolean = true): PickerLauncher {
+ // Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker
+ return if (LocalInspectionMode.current || isInTest) {
+ NoOpPickerLauncher { onResult(null) }
+ } else {
+ val context = LocalContext.current
+ val tmpFile = remember { getTemporaryFile(context) }
+ val tmpFileUri = remember(tmpFile) { getTemporaryUri(context, tmpFile) }
+ rememberPickerLauncher(type = PickerType.Camera.Photo(tmpFileUri)) { success ->
+ // Execute callback
+ onResult(if (success) tmpFileUri else null)
+ // Then remove the file and clear the picker
+ if (deleteAfter) {
+ tmpFile.delete()
+ }
+ }
+ }
+ }
+
+ /**
+ * Remembers and returns a [PickerLauncher] for recording a video with a camera app.
+ * @param [onResult] will be called with either the video's [Uri] or `null` if nothing was selected.
+ * @param [deleteAfter] When it's `true`, the recorded video will be automatically removed after calling [onResult].
+ * It's `true` by default.
+ */
+ @Composable
+ fun registerCameraVideoPicker(onResult: (Uri?) -> Unit, deleteAfter: Boolean = true): PickerLauncher {
+ // Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker
+ return if (LocalInspectionMode.current || isInTest) {
+ NoOpPickerLauncher { onResult(null) }
+ } else {
+ val context = LocalContext.current
+ val tmpFile = remember { getTemporaryFile(context) }
+ val tmpFileUri = remember(tmpFile) { getTemporaryUri(context, tmpFile) }
+ rememberPickerLauncher(type = PickerType.Camera.Video(tmpFileUri)) { success ->
+ // Execute callback
+ onResult(if (success) tmpFileUri else null)
+ // Then remove the file and clear the picker
+ if (deleteAfter) {
+ tmpFile.delete()
+ }
+ }
+ }
+ }
+
+ private fun getTemporaryFile(
+ context: Context,
+ baseFolder: File = context.cacheDir,
+ filename: String = UUID.randomUUID().toString(),
+ ): File {
+ return File(baseFolder, filename)
+ }
+
+ private fun getTemporaryUri(
+ context: Context,
+ file: File,
+ ): Uri {
+ val authority = "${context.packageName}.fileprovider"
+ return FileProvider.getUriForFile(context, authority, file)
+ }
+}
diff --git a/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerType.kt b/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerType.kt
new file mode 100644
index 0000000000..354d9a4918
--- /dev/null
+++ b/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerType.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.mediapickers
+
+import android.net.Uri
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.activity.result.contract.ActivityResultContracts
+import io.element.android.libraries.core.mimetype.MimeTypes
+
+sealed interface PickerType {
+ fun getContract(): ActivityResultContract
+ fun getDefaultRequest(): Input
+
+ object ImageAndVideo : PickerType {
+ override fun getContract() = ActivityResultContracts.PickVisualMedia()
+ override fun getDefaultRequest(): PickVisualMediaRequest {
+ return PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo)
+ }
+ }
+
+ object Camera {
+ data class Photo(val destUri: Uri) : PickerType {
+ override fun getContract() = ActivityResultContracts.TakePicture()
+ override fun getDefaultRequest(): Uri {
+ return destUri
+ }
+ }
+
+ data class Video(val destUri: Uri) : PickerType {
+ override fun getContract() = ActivityResultContracts.CaptureVideo()
+ override fun getDefaultRequest(): Uri {
+ return destUri
+ }
+ }
+ }
+
+ data class File(val mimeType: String = MimeTypes.Any) : PickerType {
+ override fun getContract() = ActivityResultContracts.GetContent()
+ override fun getDefaultRequest(): String {
+ return mimeType
+ }
+ }
+}
diff --git a/libraries/mediapickers/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTests.kt b/libraries/mediapickers/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTests.kt
new file mode 100644
index 0000000000..693ef40cfe
--- /dev/null
+++ b/libraries/mediapickers/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTests.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.mediapickers
+
+import android.net.Uri
+import androidx.activity.result.contract.ActivityResultContracts
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.core.mimetype.MimeTypes
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class PickerTypeTests {
+
+ @Test
+ fun `ImageAndVideo - assert types`() {
+ val pickerType = PickerType.ImageAndVideo
+ assertThat(pickerType.getContract()).isInstanceOf(ActivityResultContracts.PickVisualMedia::class.java)
+ assertThat(pickerType.getDefaultRequest().mediaType).isEqualTo(ActivityResultContracts.PickVisualMedia.ImageAndVideo)
+ }
+
+ @Test
+ fun `File - assert types`() {
+ val pickerType = PickerType.File()
+ assertThat(pickerType.getContract()).isInstanceOf(ActivityResultContracts.GetContent::class.java)
+ assertThat(pickerType.getDefaultRequest()).isEqualTo(MimeTypes.Any)
+
+ val mimeType = MimeTypes.Images
+ val customPickerType = PickerType.File(mimeType)
+ assertThat(customPickerType.getContract()).isInstanceOf(ActivityResultContracts.GetContent::class.java)
+ assertThat(customPickerType.getDefaultRequest()).isEqualTo(mimeType)
+ }
+
+ @Test
+ fun `CameraPhoto - assert types`() {
+ val uri = Uri.parse("file:///tmp/test")
+ val pickerType = PickerType.Camera.Photo(uri)
+ assertThat(pickerType.getContract()).isInstanceOf(ActivityResultContracts.TakePicture::class.java)
+ assertThat(pickerType.getDefaultRequest()).isEqualTo(uri)
+ }
+
+ @Test
+ fun `CameraVideo - assert types`() {
+ val uri = Uri.parse("file:///tmp/test")
+ val pickerType = PickerType.Camera.Video(uri)
+ assertThat(pickerType.getContract()).isInstanceOf(ActivityResultContracts.CaptureVideo::class.java)
+ assertThat(pickerType.getDefaultRequest()).isEqualTo(uri)
+ }
+
+}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
index 3c82496626..82e3e43d50 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
@@ -24,11 +24,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
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.notification.NotificationData
-import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
-import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
-import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
-import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
-import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.push.impl.log.pushLoggerTag
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@@ -74,28 +69,28 @@ class NotifiableEventResolver @Inject constructor(
}
).orDefault(roomId, eventId)
- return notificationData.asNotifiableEvent(sessionId, roomId, eventId)
+ return notificationData.asNotifiableEvent(sessionId)
}
}
-private fun NotificationData.asNotifiableEvent(userId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent {
+private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent {
return NotifiableMessageEvent(
sessionId = userId,
roomId = roomId,
eventId = eventId,
editedEventId = null,
canBeReplaced = true,
- noisy = false,
+ noisy = isNoisy,
timestamp = System.currentTimeMillis(),
- senderName = null,
- senderId = null,
+ senderName = senderDisplayName,
+ senderId = senderId.value,
body = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…",
imageUriString = null,
threadId = null,
roomName = null,
roomIsDirect = false,
- roomAvatarPath = null,
- senderAvatarPath = null,
+ roomAvatarPath = roomAvatarUrl,
+ senderAvatarPath = senderAvatarUrl,
soundName = null,
outGoingMessage = false,
outGoingMessageFailed = false,
@@ -109,33 +104,11 @@ private fun NotificationData.asNotifiableEvent(userId: SessionId, roomId: RoomId
*/
private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
return this ?: NotificationData(
- item = MatrixTimelineItem.Event(
- event = EventTimelineItem(
- uniqueIdentifier = eventId.value,
- eventId = eventId,
- isEditable = false,
- isLocal = false,
- isOwn = false,
- isRemote = false,
- localSendState = null,
- reactions = emptyList(),
- sender = UserId(""),
- senderProfile = ProfileTimelineDetails.Unavailable,
- timestamp = System.currentTimeMillis(),
- content = MessageContent(
- body = eventId.value,
- inReplyTo = null,
- isEdited = false,
- type = TextMessageType(
- body = eventId.value,
- formatted = null
- )
- )
- ),
- ),
- title = roomId.value,
- subtitle = eventId.value,
+ eventId = eventId,
+ senderId = UserId("@user:domain"),
+ roomId = roomId,
isNoisy = false,
- avatarUrl = null,
+ isEncrypted = false,
+ isDirect = false
)
}
diff --git a/libraries/push/impl/src/main/res/values-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..4d663c57aa
--- /dev/null
+++ b/libraries/push/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,30 @@
+
+
+ "Laute Benachrichtigungen"
+ "Beitreten"
+ "Ablehnen"
+ "Neue Nachrichten"
+ "Als gelesen markieren"
+ "%1$s: %2$s"
+ "%1$s: %2$s %3$s"
+ "%1$s in %2$s und %3$s"
+
+ - "%1$s: %2$d Nachricht"
+ - "%1$s: %2$d Nachrichten"
+
+
+ - "%d Einladung"
+ - "%d Einladungen"
+
+
+ - "%d neue Nachricht"
+ - "%d neue Nachrichten"
+
+
+ - "%d Raum"
+ - "%d Räume"
+
+ "Google-Dienste"
+ "Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig."
+ "Schnellantwort"
+
\ No newline at end of file
diff --git a/libraries/push/impl/src/main/res/values-es/translations.xml b/libraries/push/impl/src/main/res/values-es/translations.xml
new file mode 100644
index 0000000000..31df508dc3
--- /dev/null
+++ b/libraries/push/impl/src/main/res/values-es/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Respuesta rápida"
+
\ No newline at end of file
diff --git a/libraries/push/impl/src/main/res/values-it/translations.xml b/libraries/push/impl/src/main/res/values-it/translations.xml
new file mode 100644
index 0000000000..32957fe2ce
--- /dev/null
+++ b/libraries/push/impl/src/main/res/values-it/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Risposta rapida"
+
\ No newline at end of file
diff --git a/libraries/push/impl/src/main/res/values-ro/translations.xml b/libraries/push/impl/src/main/res/values-ro/translations.xml
new file mode 100644
index 0000000000..47280228a4
--- /dev/null
+++ b/libraries/push/impl/src/main/res/values-ro/translations.xml
@@ -0,0 +1,54 @@
+
+
+ "Apel"
+ "Ascultare evenimente"
+ "Notificări zgomotoase"
+ "Notificări silențioase"
+ "** Trimiterea eșuată - vă rugăm să deschideți camera"
+ "Alăturați-vă"
+ "Respingeți"
+ "Mesaje noi"
+ "Marcați ca citit"
+ "Eu"
+ "Vizualizați o notificare! Faceți clic pe mine!"
+ "%1$s: %2$s"
+ "%1$s: %2$s %3$s"
+ "%1$s și %2$s"
+ "%1$s în %2$s"
+ "%1$s în %2$s și %3$s"
+
+ - "%1$s: %2$d mesaj"
+
+ - "%1$s: %2$d mesaje"
+
+
+ - "%d notificare"
+
+ - "%d notificări"
+
+
+ - "%d invitație"
+
+ - "%d invitații"
+
+
+ - "%d mesaj nou"
+
+ - "%d mesaje noi"
+
+
+ - "%d mesaj notificat necitit"
+
+ - "%d mesaje notificate necitite"
+
+
+ - "%d cameră"
+
+ - "%d camere"
+
+ "Alegeți modul de primire a notificărilor"
+ "Sincronizare în fundal"
+ "Servicii Google"
+ "Nu au fost găsite servicii Google Play valide. Este posibil ca notificările să nu funcționeze corect."
+ "Raspuns rapid"
+
diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml
index 3a11adb5d3..d38bf7d8dd 100644
--- a/libraries/push/impl/src/main/res/values/localazy.xml
+++ b/libraries/push/impl/src/main/res/values/localazy.xml
@@ -9,7 +9,6 @@
"Reject"
"New Messages"
"Mark as read"
- "Quick reply"
"Me"
"You are viewing the notification! Click me!"
"%1$s: %2$s"
@@ -45,4 +44,5 @@
"Background synchronization"
"Google Services"
"No valid Google Play Services found. Notifications may not work properly."
+ "Quick reply"
\ No newline at end of file
diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt
index a254a636ef..df12c755e3 100644
--- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt
+++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt
@@ -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")
}
diff --git a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt
index 3481a56388..bab573de58 100644
--- a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt
+++ b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt
@@ -50,6 +50,7 @@ fun TextComposer(
onFullscreenToggle: () -> Unit = {},
onCloseSpecialMode: () -> Unit = {},
onComposerTextChange: (CharSequence) -> Unit = {},
+ onAddAttachment:() -> Unit = {},
) {
if (LocalInspectionMode.current) {
FakeComposer(modifier)
@@ -78,6 +79,7 @@ fun TextComposer(
}
override fun onAddAttachment() {
+ onAddAttachment()
}
override fun onExpandOrCompactChange() {
diff --git a/libraries/textcomposer/src/main/res/values-de/translations.xml b/libraries/textcomposer/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..f016d4bdba
--- /dev/null
+++ b/libraries/textcomposer/src/main/res/values-de/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Nachricht…"
+ "Link setzen"
+
\ No newline at end of file
diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml
index 22c60db481..4093aae962 100644
--- a/libraries/ui-strings/src/main/res/values-de/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-de/translations.xml
@@ -1,5 +1,106 @@
+ "Passwort ausblenden"
+ "Dateien senden"
+ "Passwort anzeigen"
+ "Benutzermenü"
+ "Zurück"
+ "Abbrechen"
+ "Foto auswählen"
+ "Schließen"
+ "Verifizierung abschließen"
"Bestätigen"
+ "Kopieren"
+ "Link kopieren"
+ "Erstellen"
+ "Ablehnen"
+ "Deaktivieren"
+ "Fertig"
+ "Bearbeiten"
+ "Aktivieren"
+ "Einladen"
+ "Freunde zu %1$s einladen"
+ "Einladungen"
+ "Mehr erfahren"
+ "Verlassen"
+ "Raum verlassen"
+ "Weiter"
+ "Nein"
+ "OK"
+ "Schnellantwort"
+ "Zitieren"
+ "Entfernen"
+ "Fehler melden"
+ "Inhalt melden"
+ "Erneut versuchen"
+ "Entschlüsselung erneut versuchen"
+ "Speichern"
+ "Suchen"
+ "Senden"
+ "Nachricht senden"
+ "Teilen"
+ "Link teilen"
+ "Überspringen"
+ "Foto aufnehmen"
+ "Ja"
+ "Über"
+ "Analytik"
+ "Audio"
+ "Blasen"
+ "Entschlüsselungsfehler"
+ "Entwickleroptionen"
+ "(bearbeitet)"
+ "Verschlüsselung aktiviert"
+ "Fehler"
+ "Datei"
+ "GIF"
+ "Bild"
+ "Link in Zwischenablage kopiert"
+ "Nachricht"
+ "Modern"
+ "Offline"
+ "Passwort"
+ "Reaktionen"
+ "Sicherheit"
+ "Einstellungen"
+ "Sticker"
+ "Erfolg"
+ "Vorschläge"
+ "Thema"
+ "Entschlüsselung nicht möglich"
+ "Nicht unterstütztes Ereignis"
+ "Benutzername"
+ "Verifizierung abgebrochen"
+ "Verifizierung abgeschlossen"
+ "Video"
+ "Warten…"
+ "Warnung"
+ "Aktivitäten"
+ "Flaggen"
+ "Essen & Trinken"
+ "Tiere & Natur"
+ "Objekte"
+ "Smileys & Personen"
+ "Reisen & Orte"
+ "Symbole"
+ "Fehler beim Laden der Nachrichten"
+ "Entschuldigung, ein Fehler ist aufgetreten."
+ "%1$s Android"
+
+ - "%1$d Mitglied"
+ - "%1$d Mitglieder"
+
+ "Grund für die Meldung dieses Inhalts"
+ "Dies ist der Anfang von %1$s."
+ "Neu"
+ "Blockieren"
+ "Nutzer blockieren"
+ "Blockierung aufheben"
+ "Nutzer entblockieren"
+ "Erkennungsschwelle"
+ "Version: %1$s (%2$s)"
"de"
+ "Fehler"
+ "Erfolg"
+ "Nutzer blockieren"
\ No newline at end of file
diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml
index 564ede34a8..b430048f79 100644
--- a/libraries/ui-strings/src/main/res/values-es/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-es/translations.xml
@@ -94,8 +94,6 @@
"Vídeo"
"Esperando…"
"Confirmar"
- "Error"
- "Terminado"
"Atención"
"Actividades"
"Banderas"
@@ -129,7 +127,6 @@
"Este es el principio de %1$s."
"Este es el principio de esta conversación."
"Nuevos"
- "Bloquear usuario"
"Marque si quieres ocultar todos los mensajes actuales y futuros de este usuario"
"Bloquear"
"Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento."
@@ -142,4 +139,7 @@
"General"
"Versión: %1$s (%2$s)"
"es"
+ "Error"
+ "Terminado"
+ "Bloquear usuario"
\ No newline at end of file
diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml
index a8ec05115a..8fbe54dae2 100644
--- a/libraries/ui-strings/src/main/res/values-it/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-it/translations.xml
@@ -94,8 +94,6 @@
"Video"
"In attesa…"
"Conferma"
- "Errore"
- "Operazione riuscita"
"Attenzione"
"Attività"
"Bandiere"
@@ -129,7 +127,6 @@
"Questo è l\'inizio di %1$s."
"Questo è l\'inizio della conversazione."
"Nuovo"
- "Blocca utente"
"Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente"
"Blocca"
"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."
@@ -142,4 +139,7 @@
"Generali"
"Versione: %1$s (%2$s)"
"it"
+ "Errore"
+ "Operazione riuscita"
+ "Blocca utente"
\ No newline at end of file
diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml
index 091dd7f36d..2a9dc93490 100644
--- a/libraries/ui-strings/src/main/res/values-ro/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml
@@ -4,8 +4,10 @@
"Trimiteți fișiere"
"Afișați parola"
"Meniu utilizator"
+ "Acceptați"
"Înapoi"
"Anulați"
+ "Alegeți o fotografie"
"Ștergeți"
"Închideți"
"Verificare completă"
@@ -13,13 +15,16 @@
"Continuați"
"Copiați"
"Copiați linkul"
+ "Creați"
"Creați o cameră"
+ "Refuzați"
"Dezactivați"
"Efectuat"
"Editați"
"Activați"
"Invitați"
"Invitați prieteni în %1$s"
+ "Invitații"
"Aflați mai multe"
"Părăsiți"
"Părăsiți camera"
@@ -38,15 +43,18 @@
"Salvați"
"Căutați"
"Trimiteți"
+ "Trimiteți mesajul"
"Partajați"
"Partajați linkul"
"Omiteți"
"Începeți"
"Începeți discuția"
"Începeți verificarea"
+ "Faceți o fotografie"
"Vedeți sursă"
"Da"
"Despre"
+ "Analitice"
"Audio"
"Baloane"
"Se creează camera…"
@@ -94,8 +102,6 @@
"Video"
"Se aşteaptă…"
"Confirmare"
- "Eroare"
- "Succes"
"Avertisment"
"Activități"
"Steaguri"
@@ -131,7 +137,17 @@
"Acesta este începutul conversației %1$s."
"Acesta este începutul acestei conversații."
"Nou"
- "Blocați utilizatorul"
+ "Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime."
+ "Nu"" înregistrăm sau profilăm datele contului"
+ "Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime."
+ "Puteți citi toate condițiile noastre %1$s."
+ "aici"
+ "Puteți dezactiva această opțiune oricând din setări"
+ "Nu"" împărtășim informații cu terți"
+ "Ajutați la îmbunătățirea %1$s"
+ "Puteți citi toate condițiile noastre %1$s."
+ "aici"
+ "Partajați datele analitice"
"Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator"
"Blocați"
"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."
@@ -144,4 +160,7 @@
"General"
"Versiunea: %1$s (%2$s)"
"ro"
+ "Eroare"
+ "Succes"
+ "Blocați utilizatorul"
\ No newline at end of file
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index bfd2be2000..5df318b7ae 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -102,8 +102,6 @@
"Video"
"Waiting…"
"Confirmation"
- "Error"
- "Success"
"Warning"
"Activities"
"Flags"
@@ -122,60 +120,15 @@
"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."
"Are you sure that you want to leave the room?"
"%1$s Android"
- "Call"
- "Listening for events"
- "Noisy notifications"
- "Silent notifications"
- "** Failed to send - please open room"
- "Join"
- "Reject"
- "New Messages"
- "Mark as read"
- "Quick reply"
- "Me"
- "You are viewing the notification! Click me!"
- "%1$s: %2$s"
- "%1$s: %2$s %3$s"
- "%1$s and %2$s"
- "%1$s in %2$s"
- "%1$s in %2$s and %3$s"
- "%1$d member"
- "%1$d members"
-
- - "%1$s: %2$d message"
- - "%1$s: %2$d messages"
-
-
- - "%d notification"
- - "%d notifications"
-
-
- - "%d invitation"
- - "%d invitations"
-
-
- - "%d new message"
- - "%d new messages"
-
-
- - "%d unread notified message"
- - "%d unread notified messages"
-
-
- - "%d room"
- - "%d rooms"
-
- "%1$d room change"
- "%1$d room changes"
"Rageshake to report bug"
- "Choose how to receive notifications"
- "Background synchronization"
- "Google Services"
- "No valid Google Play Services found. Notifications may not work properly."
"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"
"This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."
"Reason for reporting this content"
@@ -193,7 +146,6 @@
"You can read all our terms %1$s."
"here"
"Share analytics data"
- "Block user"
"Check if you want to hide all current and future messages from this user"
"Block"
"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."
@@ -207,4 +159,7 @@
"Version: %1$s (%2$s)"
"en"
"en"
+ "Error"
+ "Success"
+ "Block user"
\ No newline at end of file
diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt
index 5d1ef9a20e..18ce6e55f8 100644
--- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt
+++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt
@@ -90,7 +90,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
implementation(project(":libraries:di"))
implementation(project(":libraries:session-storage:impl"))
implementation(project(":libraries:statemachine"))
-
+ implementation(project(":libraries:mediapickers"))
}
fun DependencyHandlerScope.allServicesImpl() {
diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts
index 44167f906f..fda44f46d1 100644
--- a/tests/testutils/build.gradle.kts
+++ b/tests/testutils/build.gradle.kts
@@ -34,4 +34,6 @@ dependencies {
implementation(libs.coroutines.test)
implementation(projects.libraries.matrix.test)
implementation(projects.services.appnavstate.test)
+ implementation(projects.services.appnavstate.test)
+ implementation(projects.libraries.core)
}
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt
new file mode 100644
index 0000000000..1309a14cb1
--- /dev/null
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package io.element.android.tests.testutils
+
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+
+fun testCoroutineDispatchers(
+ testScheduler: TestCoroutineScheduler? = null,
+) = CoroutineDispatchers(
+ io = UnconfinedTestDispatcher(testScheduler),
+ computation = UnconfinedTestDispatcher(testScheduler),
+ main = UnconfinedTestDispatcher(testScheduler),
+ diffUpdateDispatcher = UnconfinedTestDispatcher(testScheduler),
+)
+
+fun testCoroutineDispatchers(
+ io: TestDispatcher = UnconfinedTestDispatcher(),
+ computation: TestDispatcher = UnconfinedTestDispatcher(),
+ main: TestDispatcher = UnconfinedTestDispatcher(),
+ diffUpdateDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
+) = CoroutineDispatchers(
+ io = io,
+ computation = computation,
+ main = main,
+ diffUpdateDispatcher = diffUpdateDispatcher,
+)
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index 78ce1fa217..5555d6e6b3 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a5948ff8260dc73ff19232679312141dd4021a00cce2871f69870b291a237aa9
-size 4478
+oid sha256:21698ff1c1f2b30ee9c3cc0c2539b35fe7cf54aac07cb0dc376d7c1a03c8814b
+size 4483
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index 78ce1fa217..5555d6e6b3 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a5948ff8260dc73ff19232679312141dd4021a00cce2871f69870b291a237aa9
-size 4478
+oid sha256:21698ff1c1f2b30ee9c3cc0c2539b35fe7cf54aac07cb0dc376d7c1a03c8814b
+size 4483
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_2,NEXUS_5,1.0,en].png
index 88a70ce698..c18cc0dd08 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:49aafbdb693cccd3ab7ca94e61f7a1de437766a29967dd89c4aaf5134b55c004
-size 14860
+oid sha256:db69f27f60dd9d93bb4d313741b84aa4a3ed008d229590338514c7683c0e3a11
+size 14786
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_0,NEXUS_5,1.0,en].png
index 665c8811ac..eb47d1cd71 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
-size 4457
+oid sha256:fc3bf884b0425c72cafecdd4afa4e2c28064799f695962360ae4c979a3fe542e
+size 4490
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_1,NEXUS_5,1.0,en].png
index 665c8811ac..eb47d1cd71 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
-size 4457
+oid sha256:fc3bf884b0425c72cafecdd4afa4e2c28064799f695962360ae4c979a3fe542e
+size 4490
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_2,NEXUS_5,1.0,en].png
index f7378511e1..94f6bbbd99 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:857fedd347931600aa5613565887384579b565efb86d5eeca6e93c13f5ff442c
-size 13994
+oid sha256:6c630475e03d86195a0ebcc57bd12934b799fee956c635b30df60913cd9a3f50
+size 16032
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..de77e469f6
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d8c78efed997712873719636ff1f8479d38d317e443c5d4340346d4328de9c0d
+size 28744
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..de77e469f6
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d8c78efed997712873719636ff1f8479d38d317e443c5d4340346d4328de9c0d
+size 28744
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..f269338d7c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2f62537b4aa79f501908d1fff9d269139e88db5f6dbaedcf63670a4f71b47bff
+size 28303
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..f269338d7c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2f62537b4aa79f501908d1fff9d269139e88db5f6dbaedcf63670a4f71b47bff
+size 28303
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_5,NEXUS_5,1.0,en].png
index e9e352cb20..4ec6a0f362 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:354452861d1e006a8bfa744251ffdaf15088e0bb181a53043f121e606233d648
-size 67340
+oid sha256:a8a9186d741d251dc8bdf6bcf71d395e8c057c310585aeb8911609eaaacce842
+size 64550
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_6,NEXUS_5,1.0,en].png
index 8dd1cd9116..4ec6a0f362 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f1cb6bac9e72b956d0cffde807340e36a7a4d6873d4d7337995b53e82769c4f9
-size 68135
+oid sha256:a8a9186d741d251dc8bdf6bcf71d395e8c057c310585aeb8911609eaaacce842
+size 64550
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_5,NEXUS_5,1.0,en].png
index 815e64ba9f..562eb63427 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dc56e29c26250c6fef312ec4c5fdfaa2f63159fd0565bd37522b46c7ff67906a
-size 61924
+oid sha256:94a589b556a750485fd61af6446457c98c5f112d0d013cd78016b88a8829e6a8
+size 58643
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_6,NEXUS_5,1.0,en].png
index 687b034bee..562eb63427 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c6faff25a1c187dee59d0dcf0affd5029251512efb6eff7fc2d41d34bace2061
-size 62356
+oid sha256:94a589b556a750485fd61af6446457c98c5f112d0d013cd78016b88a8829e6a8
+size 58643
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarDarkPreview_0_null,NEXUS_5,1.0,en].png
index 167599e090..ec4ab9f3e7 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarDarkPreview_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarDarkPreview_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8bca09418758a20493dae2e73a747449af8a448ad3a3cc4c5aae2e08a425f3fb
-size 13464
+oid sha256:b5f2b24a19ca49b3e6e34ccd65d2bdba72d0384104931bda92e191959d58c5c3
+size 12697
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarLightPreview_0_null,NEXUS_5,1.0,en].png
index 7efc250776..ddbf6df0b5 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarLightPreview_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarLightPreview_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e50325c75193e47958862ea9cb515d7c84d2c47a00b01256fc244319780c107f
-size 12425
+oid sha256:c4fa32eb24a0cc51b9b19c6f24a7d3d59aae65f1f30a43b1a6d70b3ed3e2154d
+size 11716
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListSearchResultContentPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListSearchResultContentPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..b113ba20f0
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListSearchResultContentPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:97b11203623c0c98da88dfedf85cb80d1f35cc55da570e12061b1385691bf1f0
+size 27758
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index 4e36616412..e4b5f95b8d 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0fc272268179409483fc5fad89aa00714a6811c63e478b5c696d4ab0338ce6bf
-size 37781
+oid sha256:3cdb131c68de1fce5a3319151e39148e9f3a71c7bc3984e89ec0a80abf0f7288
+size 37044
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index 0d3bec9cf5..1dcb6593b1 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d729c75d73d1837b365c8332b6e4203cb2492f6c5c4af06741a4bd2e818daebb
-size 60667
+oid sha256:8b265978c4db7b266fd07d56364eccafba1cd765ed9bf6d5a03b1584e173ba6a
+size 59936
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
index 4e36616412..e4b5f95b8d 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0fc272268179409483fc5fad89aa00714a6811c63e478b5c696d4ab0338ce6bf
-size 37781
+oid sha256:3cdb131c68de1fce5a3319151e39148e9f3a71c7bc3984e89ec0a80abf0f7288
+size 37044
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
index c454377b9b..52c50a59a1 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:56a253a7981823ca0fc7b653f9e83d29d1f259fea43ca4cee5760fa863306f2c
-size 39847
+oid sha256:ccf989dac7fad3cc70443d96e1ebd519463a6559ed0795ae2a1ffbaf91bdfe7c
+size 39092
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
index 2063e4eeee..e7c9ed7df3 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2b1222e1ef2d0739caa410540bf74fea681ef2e05bb04b976e50f1fe5256d613
-size 39762
+oid sha256:c2a23141c6cc8aa6e7e5f0757bda4d1117bf7f752412ccc4649ea560113b3e3f
+size 39030
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..069af4cecf
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9095546c30bb5bc9800c852456fa9cd82d14e873a5e1488d29496af088e951da
+size 4882
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..9861b7572e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8cfe321535e1ce223a0460435123dc59e74c436bdd8696cf4bdb2169f511832b
+size 28541
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
index 4a51d9cddc..2aa0933d88 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:43b69859fa3ee38d2b7f7415b87738db65dc6dac3d2fabddc1f1346b0b64932b
-size 37329
+oid sha256:a2a261b30866af95b856ee1e7d6ac2cbe2d638cf80277645f361b043d2f94e60
+size 36658
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
index 92729ff9ed..2fcea72749 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f61d4fc4d5af166e1761f004e7d80934eef24a582c96c590c769ed1fb13041a4
-size 59489
+oid sha256:0cdd1fad4b3db78fb8599785334366e3bfbaf5dead7990f396f8752862ab9e98
+size 58987
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
index 4a51d9cddc..2aa0933d88 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:43b69859fa3ee38d2b7f7415b87738db65dc6dac3d2fabddc1f1346b0b64932b
-size 37329
+oid sha256:a2a261b30866af95b856ee1e7d6ac2cbe2d638cf80277645f361b043d2f94e60
+size 36658
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
index 1287c0ec4f..2664376698 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1bf9529823b4e04261b1528b2309c549f1d3aad80c542059f11c1b26a2979b04
-size 39359
+oid sha256:1ba3d8a5cfbd102dc6df8f511eb14663170a6d550ee5e65702bc1f3fce3efd14
+size 38657
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
index 273e7af0cf..5793650c81 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ebb15a58e5d3497b2819f5a6b6fde88962f01cc2ae02d4b8cda0e45e1dde677f
-size 39314
+oid sha256:d427b479f9eb6227bb92aecd997bab97a735ef85dd279a575d777e754effd258
+size 38639
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..6c14f8062c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d463ab6d045cc310973c5ff900cf4d9ae04e93cb1c7eac3f9b2aa0ea9b827cee
+size 4815
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..b113ba20f0
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:97b11203623c0c98da88dfedf85cb80d1f35cc55da570e12061b1385691bf1f0
+size 27758
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png
index a4aa94b6e4..a64e1b5a7a 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ddbb6611ae83055106f7b67ec828542f8a896cafb49001ed0baef43633cc77c1
-size 8884
+oid sha256:428372bd789ff5e83ba4d837bbb0592edd8a01571728c7b284c57ddb200226ed
+size 9407
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png
index 317643c598..9aa01dc971 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5c9a3c9f68a6627654856b03d2534ae1e4e8e600989bc3719407fbf8e17a7ab1
-size 8631
+oid sha256:718cf6e3323ceb9bbf8b4dd9752203d5137840bd3dfb538008d579511b177412
+size 9504