Merge branch 'develop' into feature/fga/image_loading
This commit is contained in:
commit
4b60b14550
182 changed files with 2492 additions and 884 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
maestro-cloud:
|
||||
name: Maestro test suite
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.review.state == 'approved'
|
||||
if: github.event.review.state == 'approved' || github.event_name == 'workflow_dispatch'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Allow one per PR.
|
||||
|
|
|
|||
4
.github/workflows/quality.yml
vendored
4
.github/workflows/quality.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
|
@ -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' }}
|
||||
|
||||
|
|
|
|||
67
.github/workflows/triage-labelled.yml
vendored
67
.github/workflows/triage-labelled.yml
vendored
|
|
@ -12,22 +12,10 @@ jobs:
|
|||
if: >
|
||||
github.repository == 'vector-im/element-x-android'
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||
query: |
|
||||
mutation add_to_project($projectid:ID!,$contentid:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
projectid: ${{ env.PROJECT_ID }}
|
||||
contentid: ${{ github.event.issue.node_id }}
|
||||
env:
|
||||
PROJECT_ID: "PVT_kwDOAM0swc4ABTXY"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/43
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
ex_plorers:
|
||||
name: Add labelled issues to X-Plorer project
|
||||
|
|
@ -35,23 +23,10 @@ jobs:
|
|||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'Team: Element X Feature')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||
query: |
|
||||
mutation add_to_project($projectid:ID!,$contentid:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
projectid: ${{ env.PROJECT_ID }}
|
||||
contentid: ${{ github.event.issue.node_id }}
|
||||
env:
|
||||
PROJECT_ID: "PVT_kwDOAM0swc4ALoFY"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/73
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
verticals_feature:
|
||||
name: Add labelled issues to Verticals Feature project
|
||||
|
|
@ -59,20 +34,18 @@ jobs:
|
|||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'Team: Verticals Feature')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||
query: |
|
||||
mutation add_to_project($projectid:ID!,$contentid:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
projectid: ${{ env.PROJECT_ID }}
|
||||
contentid: ${{ github.event.issue.node_id }}
|
||||
env:
|
||||
PROJECT_ID: "PVT_kwDOAM0swc4AHJKW"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/57
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
qa:
|
||||
name: Add labelled issues to QA project
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'Team: QA')
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
project-url: https://github.com/orgs/vector-im/projects/69
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
|
|
|||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.10" />
|
||||
<option name="version" value="1.8.20" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
appId: ${APP_ID}
|
||||
---
|
||||
- tapOn: "Settings"
|
||||
- tapOn:
|
||||
id: "home_screen-settings"
|
||||
- tapOn: "Sign out"
|
||||
- takeScreenshot: build/maestro/900-SignOutDialg
|
||||
# Ensure cancel cancels
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
appId: ${APP_ID}
|
||||
---
|
||||
- tapOn: "Settings"
|
||||
- tapOn:
|
||||
id: "home_screen-settings"
|
||||
- assertVisible: "Rageshake to report bug"
|
||||
- takeScreenshot: build/maestro/600-Settings
|
||||
- tapOn:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -28,6 +28,16 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.ElementX"
|
||||
tools:targetApi="33">
|
||||
|
||||
<provider
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_providers" />
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ package io.element.android.x
|
|||
import android.app.Application
|
||||
import androidx.startup.AppInitializer
|
||||
import io.element.android.x.di.AppComponent
|
||||
import io.element.android.x.di.DaggerAppComponent
|
||||
import io.element.android.libraries.di.DaggerComponentOwner
|
||||
import io.element.android.x.di.DaggerAppComponent
|
||||
import io.element.android.x.info.logApplicationInfo
|
||||
import io.element.android.x.initializer.CrashInitializer
|
||||
import io.element.android.x.initializer.MatrixInitializer
|
||||
|
|
|
|||
19
app/src/main/res/xml/file_providers.xml
Normal file
19
app/src/main/res/xml/file_providers.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path name="cache" path="." />
|
||||
</paths>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1
changelog.d/339.feature
Normal file
1
changelog.d/339.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Block & unblock users from room details screen.
|
||||
1
changelog.d/354.feature
Normal file
1
changelog.d/354.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Improve room list search and general UI
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_create_room_action_create_room">"Neuer Raum"</string>
|
||||
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Raumname"</string>
|
||||
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
|
||||
</resources>
|
||||
|
|
@ -3,6 +3,16 @@
|
|||
<string name="screen_create_room_action_create_room">"Cameră nouă"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Invitați persoane"</string>
|
||||
<string name="screen_create_room_add_people_title">"Adaugați persoane"</string>
|
||||
<string name="screen_create_room_error_creating_room">"A apărut o eroare la crearea camerei"</string>
|
||||
<string name="screen_create_room_private_option_description">"Mesajele din această cameră sunt criptate. Criptarea nu poate fi dezactivată ulterior."</string>
|
||||
<string name="screen_create_room_private_option_title">"Cameră privată (doar pe bază de invitație)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Mesajele nu sunt criptate și oricine le poate citi. Puteți activa criptarea la o dată ulterioară."</string>
|
||||
<string name="screen_create_room_public_option_title">"Cameră publică (oricine)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Numele camerei"</string>
|
||||
<string name="screen_create_room_room_name_placeholder">"e.g. Mici și Cozonaci"</string>
|
||||
<string name="screen_create_room_title">"Creați o cameră"</string>
|
||||
<string name="screen_create_room_topic_label">"Subiect (opțional)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Despre ce este această cameră?"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string>
|
||||
<string name="screen_start_chat_unknown_profile">"Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită."</string>
|
||||
</resources>
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_decline_direct_chat_title">"Chat ablehnen"</string>
|
||||
<string name="screen_invites_empty_list">"Keine Einladungen"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s hat dich eingeladen"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s te invitó."</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s ti ha invitato"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_decline_chat_message">"Sigur doriți să refuzați alăturarea la %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Refuzați invitația"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Sigur doriți să refuzați conversațiile cu %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Refuzați conversația"</string>
|
||||
<string name="screen_invites_empty_list">"Nicio invitație"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s v-a invitat"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_change_server_subtitle">"Wie lautet die Adresse deines Servers?"</string>
|
||||
<string name="screen_login_title">"Willkommen zurück!"</string>
|
||||
<string name="screen_login_password_hint">"Passwort"</string>
|
||||
<string name="screen_login_username_hint">"Benutzername"</string>
|
||||
</resources>
|
||||
|
|
@ -4,17 +4,17 @@
|
|||
<string name="screen_change_server_error_no_sliding_sync_message">"Este servidor no soporta sliding sync."</string>
|
||||
<string name="screen_change_server_form_header">"Dirección del homeserver"</string>
|
||||
<string name="screen_change_server_form_notice">"Solo puedes conectarte a un servidor que soporte sliding sync. El administrador de tu servidor tendrá que configurarlo. %1$s"</string>
|
||||
<string name="screen_change_server_submit">"Continuar"</string>
|
||||
<string name="screen_change_server_subtitle">"¿Cuál es la dirección de tu servidor?"</string>
|
||||
<string name="screen_change_server_title">"Selecciona tu servidor"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Esta cuenta ha sido desactivada."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Usuario y/o contraseña incorrectos"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Este no es un id de usuario válido. Formato esperado: \'@user:homeserver.org\'"</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"El servidor seleccionado no admite contraseñas ni inicio de sesión OIDC. Póngase en contacto con su administrador o elija otro homeserver."</string>
|
||||
<string name="screen_login_form_header">"Introduce tus datos"</string>
|
||||
<string name="screen_login_password_hint">"Contraseña"</string>
|
||||
<string name="screen_login_server_header">"Donde viven tus conversaciones"</string>
|
||||
<string name="screen_login_submit">"Continuar"</string>
|
||||
<string name="screen_login_title">"¡Hola de nuevo!"</string>
|
||||
<string name="screen_change_server_submit">"Continuar"</string>
|
||||
<string name="screen_change_server_title">"Selecciona tu servidor"</string>
|
||||
<string name="screen_login_password_hint">"Contraseña"</string>
|
||||
<string name="screen_login_submit">"Continuar"</string>
|
||||
<string name="screen_login_username_hint">"Usuario"</string>
|
||||
</resources>
|
||||
|
|
@ -4,17 +4,17 @@
|
|||
<string name="screen_change_server_error_no_sliding_sync_message">"Questo server attualmente non supporta la sincronizzazione scorrevole."</string>
|
||||
<string name="screen_change_server_form_header">"URL dell\'homeserver"</string>
|
||||
<string name="screen_change_server_form_notice">"Puoi connetterti solo a un server esistente che supporta la sincronizzazione scorrevole. L\'amministratore del tuo server domestico dovrà configurarlo. %1$s"</string>
|
||||
<string name="screen_change_server_submit">"Continua"</string>
|
||||
<string name="screen_change_server_subtitle">"Qual è l\'indirizzo del tuo server?"</string>
|
||||
<string name="screen_change_server_title">"Seleziona il tuo server"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Questo profilo è stato disattivato."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Nome utente e/o password errati"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Questo non è un identificatore utente valido. Formato previsto: \'@user:homeserver.org\'"</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"L\'homeserver selezionato non supporta la password o l\'accesso OIDC. Contatta il tuo amministratore o scegli un altro homeserver."</string>
|
||||
<string name="screen_login_form_header">"Inserisci i tuoi dati"</string>
|
||||
<string name="screen_login_password_hint">"Password"</string>
|
||||
<string name="screen_login_server_header">"Dove vivono le tue conversazioni"</string>
|
||||
<string name="screen_login_submit">"Continua"</string>
|
||||
<string name="screen_login_title">"Bentornato!"</string>
|
||||
<string name="screen_change_server_submit">"Continua"</string>
|
||||
<string name="screen_change_server_title">"Seleziona il tuo server"</string>
|
||||
<string name="screen_login_password_hint">"Password"</string>
|
||||
<string name="screen_login_submit">"Continua"</string>
|
||||
<string name="screen_login_username_hint">"Nome utente"</string>
|
||||
</resources>
|
||||
|
|
@ -4,17 +4,17 @@
|
|||
<string name="screen_change_server_error_no_sliding_sync_message">"Momentan acest server nu oferă suport pentru sliding sync."</string>
|
||||
<string name="screen_change_server_form_header">"Adresa URL a homeserver-ului"</string>
|
||||
<string name="screen_change_server_form_notice">"Vă putețo conecta numai la un server existent care oferă suport pentru sliding sync. Administratorul homeserver-ului dumneavoastră va trebui să îl configureze. %1$s"</string>
|
||||
<string name="screen_change_server_submit">"Continuați"</string>
|
||||
<string name="screen_change_server_subtitle">"Care este adresa serverului dumneavoastră?"</string>
|
||||
<string name="screen_change_server_title">"Selectați serverul"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Acest cont a fost dezactivat."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Utilizator și/sau parolă incorecte"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Acesta nu este un identificator de utilizator valid. Format așteptat: „@user:homeserver.org”"</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"Homeserver-ul selectat nu acceptă autentificarea prin parola sau OIDC. Te rugăm să contactezi administratorul sau să alegi un alt homeserver."</string>
|
||||
<string name="screen_login_form_header">"Introduceți detaliile"</string>
|
||||
<string name="screen_login_password_hint">"Parolă"</string>
|
||||
<string name="screen_login_server_header">"Locul unde trăiesc conversațiile tale"</string>
|
||||
<string name="screen_login_submit">"Continuați"</string>
|
||||
<string name="screen_login_title">"Bine ați revenit!"</string>
|
||||
<string name="screen_change_server_submit">"Continuați"</string>
|
||||
<string name="screen_change_server_title">"Selectați serverul"</string>
|
||||
<string name="screen_login_password_hint">"Parola"</string>
|
||||
<string name="screen_login_submit">"Continuați"</string>
|
||||
<string name="screen_login_username_hint">"Utilizator"</string>
|
||||
</resources>
|
||||
|
|
@ -4,17 +4,17 @@
|
|||
<string name="screen_change_server_error_no_sliding_sync_message">"This server currently doesn’t support sliding sync."</string>
|
||||
<string name="screen_change_server_form_header">"Homeserver URL"</string>
|
||||
<string name="screen_change_server_form_notice">"You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$s"</string>
|
||||
<string name="screen_change_server_submit">"Continue"</string>
|
||||
<string name="screen_change_server_subtitle">"What is the address of your server?"</string>
|
||||
<string name="screen_change_server_title">"Select your server"</string>
|
||||
<string name="screen_login_error_deactivated_account">"This account has been deactivated."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Incorrect username and/or password"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"This is not a valid user identifier. Expected format: ‘@user:homeserver.org’"</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"The selected homeserver doesn\'t support password or OIDC login. Please contact your admin or choose another homeserver."</string>
|
||||
<string name="screen_login_form_header">"Enter your details"</string>
|
||||
<string name="screen_login_password_hint">"Password"</string>
|
||||
<string name="screen_login_server_header">"Where your conversations live"</string>
|
||||
<string name="screen_login_submit">"Continue"</string>
|
||||
<string name="screen_login_title">"Welcome back!"</string>
|
||||
<string name="screen_change_server_submit">"Continue"</string>
|
||||
<string name="screen_change_server_title">"Select your server"</string>
|
||||
<string name="screen_login_password_hint">"Password"</string>
|
||||
<string name="screen_login_submit">"Continue"</string>
|
||||
<string name="screen_login_username_hint">"Username"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_signout_confirmation_dialog_submit">"Abmelden"</string>
|
||||
<string name="screen_signout_confirmation_dialog_title">"Abmelden"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"Abmeldung läuft…"</string>
|
||||
<string name="screen_signout_preference_item">"Abmelden"</string>
|
||||
</resources>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = { _, _ -> }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MessageComposerState> {
|
||||
|
||||
@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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ internal class CacheInvalidator(private val itemStatesCache: MutableList<Timelin
|
|||
ListUpdateCallback {
|
||||
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
Timber.v("onChanged(position= $position, count= $count")
|
||||
Timber.d("onChanged(position= $position, count= $count)")
|
||||
(position until position + count).forEach {
|
||||
// Invalidate cache
|
||||
itemStatesCache[it] = null
|
||||
|
|
@ -33,13 +33,13 @@ internal class CacheInvalidator(private val itemStatesCache: MutableList<Timelin
|
|||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
Timber.v("onMoved(fromPosition= $fromPosition, toPosition= $toPosition")
|
||||
Timber.d("onMoved(fromPosition= $fromPosition, toPosition= $toPosition)")
|
||||
val model = itemStatesCache.removeAt(fromPosition)
|
||||
itemStatesCache.add(toPosition, model)
|
||||
}
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
Timber.v("onInserted(position= $position, count= $count")
|
||||
Timber.d("onInserted(position= $position, count= $count)")
|
||||
itemStatesCache.invalidateLast()
|
||||
repeat(count) {
|
||||
itemStatesCache.add(position, null)
|
||||
|
|
@ -47,7 +47,7 @@ internal class CacheInvalidator(private val itemStatesCache: MutableList<Timelin
|
|||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
Timber.v("onRemoved(position= $position, count= $count")
|
||||
Timber.d("onRemoved(position= $position, count= $count)")
|
||||
itemStatesCache.invalidateLast()
|
||||
repeat(count) {
|
||||
itemStatesCache.removeAt(position)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ sealed interface TimelineItem {
|
|||
is Virtual -> id
|
||||
}
|
||||
|
||||
fun contentType(): String = when (this) {
|
||||
is Event -> content.type
|
||||
is Virtual -> model.type
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class Virtual(
|
||||
val id: String,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,4 +23,6 @@ data class TimelineItemImageContent(
|
|||
val mediaRequestData: MediaRequestData,
|
||||
val blurhash: String?,
|
||||
val aspectRatio: Float
|
||||
) : TimelineItemEventContent
|
||||
) : TimelineItemEventContent{
|
||||
override val type: String = "TimelineItemImageContent"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -49,23 +49,6 @@ class TimelinePresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - makes sure timeline is initialized and disposed`() = runTest {
|
||||
val fakeTimeline = FakeMatrixTimeline()
|
||||
val presenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
room = FakeMatrixRoom(matrixTimeline = fakeTimeline),
|
||||
)
|
||||
assertThat(fakeTimeline.isInitialized).isFalse()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(2)
|
||||
assertThat(fakeTimeline.isInitialized).isTrue()
|
||||
}
|
||||
assertThat(fakeTimeline.isInitialized).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - load more`() = runTest {
|
||||
val presenter = TimelinePresenter(
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ class NetworkMonitorImpl @Inject constructor(
|
|||
|
||||
private fun listenToConnectionChanges() {
|
||||
val request = NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
// .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
// .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_onboarding_welcome_title">"Sei in deinem Element"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bug_report_editor_placeholder">"Beschreibe den Fehler…"</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Absturzprotokolle senden"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Bildschirmfoto senden"</string>
|
||||
</resources>
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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<RoomMemberListNode>(buildContext, listOf(roomMemberListCallback))
|
||||
}
|
||||
is NavTarget.RoomMemberDetails -> {
|
||||
createNode<RoomMemberDetailsNode>(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMember)))
|
||||
createNode<RoomMemberDetailsNode>(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMemberId)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<RoomDetailsState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomDetailsState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var leaveRoomWarning by remember {
|
||||
val leaveRoomWarning = remember {
|
||||
mutableStateOf<LeaveRoomWarning?>(null)
|
||||
}
|
||||
var error by remember {
|
||||
val error = remember {
|
||||
mutableStateOf<RoomDetailsError?>(null)
|
||||
}
|
||||
|
||||
var memberCount: Async<Int> 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<RoomDetailsType> = remember(dmMember) {
|
||||
derivedStateOf {
|
||||
if (dmMember != null) {
|
||||
RoomDetailsType.Dm(dmMember)
|
||||
} else {
|
||||
RoomDetailsType.Room
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getMemberCount(membersState: MatrixRoomMembersState): State<Async<Int>> {
|
||||
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<Int>,
|
||||
leaveRoomWarning: MutableState<LeaveRoomWarning?>,
|
||||
error: MutableState<RoomDetailsError?>,
|
||||
) = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,10 +16,8 @@
|
|||
|
||||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.isLoading
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
data class RoomDetailsState(
|
||||
|
|
@ -33,6 +31,7 @@ data class RoomDetailsState(
|
|||
val displayLeaveRoomWarning: LeaveRoomWarning?,
|
||||
val error: RoomDetailsError?,
|
||||
val roomType: RoomDetailsType,
|
||||
val roomMemberDetailsState: RoomMemberDetailsState?,
|
||||
val eventSink: (RoomDetailsEvent) -> Unit
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -71,5 +71,6 @@ fun aRoomDetailsState() = RoomDetailsState(
|
|||
displayLeaveRoomWarning = null,
|
||||
error = null,
|
||||
roomType = RoomDetailsType.Room,
|
||||
roomMemberDetailsState = null,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ import androidx.compose.ui.res.vectorResource
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomdetails.impl.members.details.BlockSection
|
||||
import io.element.android.features.roomdetails.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.roomdetails.blockuser.BlockUserSection
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberShareSection
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
|
@ -116,7 +117,7 @@ fun RoomDetailsView(
|
|||
}
|
||||
|
||||
if (state.roomType is RoomDetailsType.Room) {
|
||||
val memberCount = (state.memberCount as? Async.Success<Int>)?.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<Plugin>,
|
||||
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<Callback>()
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<RoomMemberListState> {
|
||||
|
||||
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<ImmutableList<MatrixUser>>>(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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ImmutableList<MatrixUser>>,
|
||||
val userListState: UserListState,
|
||||
// val eventSink: (AddPeopleEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<MatrixUser> {
|
||||
return room.members().filter { member ->
|
||||
if (query.isBlank()) {
|
||||
true
|
||||
} else {
|
||||
override suspend fun search(query: String): List<MatrixUser> = 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(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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,
|
||||
|
|
|
|||
|
|
@ -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<RoomMemberDetailsState> {
|
||||
|
||||
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<ConfirmationDialog?>(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<Boolean>) = launch {
|
||||
client.ignoreUser(userId)
|
||||
.map {
|
||||
isBlockedState.value = true
|
||||
room.updateMembers()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState<Boolean>) = launch {
|
||||
client.unignoreUser(userId)
|
||||
.map {
|
||||
isBlockedState.value = false
|
||||
room.updateMembers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,5 +21,11 @@ data class RoomMemberDetailsState(
|
|||
val userName: String?,
|
||||
val avatarUrl: String?,
|
||||
val isBlocked: Boolean,
|
||||
// val eventSink: (RoomMemberDetailsEvents) -> Unit
|
||||
)
|
||||
val displayConfirmationDialog: ConfirmationDialog? = null,
|
||||
val isCurrentUser: Boolean,
|
||||
val eventSink: (RoomMemberDetailsEvents) -> Unit
|
||||
) {
|
||||
enum class ConfirmationDialog {
|
||||
Block, Unblock
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ open class RoomMemberDetailsStateProvider : PreviewParameterProvider<RoomMemberD
|
|||
aRoomMemberDetailsState(),
|
||||
aRoomMemberDetailsState().copy(userName = null),
|
||||
aRoomMemberDetailsState().copy(isBlocked = true),
|
||||
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block),
|
||||
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock),
|
||||
// Add other states here
|
||||
)
|
||||
}
|
||||
|
|
@ -33,5 +35,6 @@ fun aRoomMemberDetailsState() = RoomMemberDetailsState(
|
|||
userName = "Daniel",
|
||||
avatarUrl = null,
|
||||
isBlocked = false,
|
||||
// eventSink = {},
|
||||
isCurrentUser = false,
|
||||
eventSink = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,12 +39,15 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomdetails.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.roomdetails.blockuser.BlockUserSection
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
|
|
@ -86,9 +89,10 @@ fun RoomMemberDetailsView(
|
|||
// TODO implement send DM
|
||||
})
|
||||
|
||||
BlockSection(isBlocked = state.isBlocked, onToggleBlock = {
|
||||
// TODO implement block & unblock
|
||||
})
|
||||
if (!state.isCurrentUser) {
|
||||
BlockUserSection(state)
|
||||
BlockUserDialogs(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -139,24 +143,6 @@ internal fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier =
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun BlockSection(isBlocked: Boolean, onToggleBlock: () -> Unit, modifier: Modifier = Modifier) {
|
||||
PreferenceCategory(showDivider = false, modifier = modifier) {
|
||||
if (isBlocked) {
|
||||
PreferenceText(
|
||||
title = stringResource(R.string.screen_dm_details_unblock_user),
|
||||
icon = Icons.Outlined.Block,
|
||||
)
|
||||
} else {
|
||||
PreferenceText(
|
||||
title = stringResource(R.string.screen_dm_details_block_user),
|
||||
icon = Icons.Outlined.Block,
|
||||
tintColor = LocalColors.current.textActionCritical,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"1 Person"</item>
|
||||
<item quantity="other">"%1$d Personen"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_share_room_title">"Raum teilen"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Blockieren"</string>
|
||||
<string name="screen_dm_details_block_user">"Nutzer blockieren"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Blockierung aufheben"</string>
|
||||
<string name="screen_dm_details_unblock_user">"Nutzer entblockieren"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Raum verlassen"</string>
|
||||
<string name="screen_room_details_security_title">"Sicherheit"</string>
|
||||
<string name="screen_room_details_topic_title">"Thema"</string>
|
||||
</resources>
|
||||
|
|
@ -4,18 +4,18 @@
|
|||
<item quantity="one">"Una persona"</item>
|
||||
<item quantity="other">"%1$d personas"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Cifrado de mensajes activado"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invitar a otras personas"</string>
|
||||
<string name="screen_room_details_share_room_title">"Compartir sala"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Bloquear"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puedes revertir esta acción en cualquier momento."</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento."</string>
|
||||
<string name="screen_dm_details_block_user">"Bloquear usuario"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Desbloquear"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"Al desbloquear al usuario, podrás volver a ver todos sus mensajes."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Desbloquear usuario"</string>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Cifrado de mensajes activado"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invitar a otras personas"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Salir de la sala"</string>
|
||||
<string name="screen_room_details_people_title">"Personas"</string>
|
||||
<string name="screen_room_details_security_title">"Seguridad"</string>
|
||||
<string name="screen_room_details_share_room_title">"Compartir sala"</string>
|
||||
<string name="screen_room_details_topic_title">"Tema"</string>
|
||||
</resources>
|
||||
|
|
@ -4,18 +4,18 @@
|
|||
<item quantity="one">"1 persona"</item>
|
||||
<item quantity="other">"%1$d persone"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Crittografia messaggi abilitata"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invita persone"</string>
|
||||
<string name="screen_room_details_share_room_title">"Condividi stanza"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Blocca"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti i loro messaggi saranno nascosti. Potrai annullare questa azione in qualsiasi momento."</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento."</string>
|
||||
<string name="screen_dm_details_block_user">"Blocca utente"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Sblocca"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Sblocca utente"</string>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Crittografia messaggi abilitata"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invita persone"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Esci dalla stanza"</string>
|
||||
<string name="screen_room_details_people_title">"Persone"</string>
|
||||
<string name="screen_room_details_security_title">"Sicurezza"</string>
|
||||
<string name="screen_room_details_share_room_title">"Condividi stanza"</string>
|
||||
<string name="screen_room_details_topic_title">"Oggetto"</string>
|
||||
</resources>
|
||||
|
|
@ -5,18 +5,18 @@
|
|||
<item quantity="few"></item>
|
||||
<item quantity="other">"%1$d persoane"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Criptarea mesajelor este activată"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invitați persoane"</string>
|
||||
<string name="screen_room_details_share_room_title">"Partajați camera"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Blocați"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."</string>
|
||||
<string name="screen_dm_details_block_user">"Blocați utilizatorul"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Deblocați"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Deblocați utilizatorul"</string>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Criptarea mesajelor este activată"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invitați persoane"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Părăsiți camera"</string>
|
||||
<string name="screen_room_details_people_title">"Persoane"</string>
|
||||
<string name="screen_room_details_security_title">"Securitate"</string>
|
||||
<string name="screen_room_details_share_room_title">"Partajați camera"</string>
|
||||
<string name="screen_room_details_topic_title">"Subiect"</string>
|
||||
</resources>
|
||||
|
|
@ -4,18 +4,18 @@
|
|||
<item quantity="one">"1 person"</item>
|
||||
<item quantity="other">"%1$d people"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invite people"</string>
|
||||
<string name="screen_room_details_share_room_title">"Share room"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Block"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
|
||||
<string name="screen_dm_details_block_user">"Block user"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Unblock"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"On unblocking the user, you will be able to see all messages by them again."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Unblock user"</string>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invite people"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Leave room"</string>
|
||||
<string name="screen_room_details_people_title">"People"</string>
|
||||
<string name="screen_room_details_security_title">"Security"</string>
|
||||
<string name="screen_room_details_share_room_title">"Share room"</string>
|
||||
<string name="screen_room_details_topic_title">"Topic"</string>
|
||||
</resources>
|
||||
|
|
@ -23,9 +23,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<RoomMember> = 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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,4 +20,5 @@ sealed interface RoomListEvents {
|
|||
data class UpdateFilter(val newFilter: String) : RoomListEvents
|
||||
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
|
||||
object DismissRequestVerificationPrompt : RoomListEvents
|
||||
object ToggleSearchResults : RoomListEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ class RoomListPresenter @Inject constructor(
|
|||
|
||||
Timber.v("RoomSummaries size = ${roomSummaries.size}")
|
||||
|
||||
val mappedRoomSummaries: MutableState<ImmutableList<RoomListRoomSummary>> = remember { mutableStateOf(persistentListOf()) }
|
||||
val filteredRoomSummaries: MutableState<ImmutableList<RoomListRoomSummary>> = remember {
|
||||
mutableStateOf(persistentListOf())
|
||||
}
|
||||
|
|
@ -101,41 +102,51 @@ class RoomListPresenter @Inject constructor(
|
|||
derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified && !verificationPromptDismissed }
|
||||
}
|
||||
|
||||
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
fun handleEvents(event: RoomListEvents) {
|
||||
when (event) {
|
||||
is RoomListEvents.UpdateFilter -> filter = event.newFilter
|
||||
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
|
||||
RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true
|
||||
RoomListEvents.ToggleSearchResults -> {
|
||||
if (displaySearchResults) {
|
||||
filter = ""
|
||||
}
|
||||
displaySearchResults =! displaySearchResults
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(roomSummaries, filter) {
|
||||
filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter)
|
||||
mappedRoomSummaries.value = if (roomSummaries.isEmpty()) {
|
||||
RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList()
|
||||
} else {
|
||||
mapRoomSummaries(roomSummaries).toImmutableList()
|
||||
}
|
||||
filteredRoomSummaries.value = updateFilteredRoomSummaries(mappedRoomSummaries.value, filter)
|
||||
}
|
||||
|
||||
val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
|
||||
|
||||
return RoomListState(
|
||||
matrixUser = matrixUser.value,
|
||||
roomList = filteredRoomSummaries.value,
|
||||
roomList = mappedRoomSummaries.value,
|
||||
filter = filter,
|
||||
filteredRoomList = filteredRoomSummaries.value,
|
||||
displayVerificationPrompt = displayVerificationPrompt,
|
||||
snackbarMessage = snackbarMessage,
|
||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||
displayInvites = invites.isNotEmpty(),
|
||||
displaySearchResults = displaySearchResults,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun updateFilteredRoomSummaries(roomSummaries: List<RoomSummary>?, filter: String): ImmutableList<RoomListRoomSummary> {
|
||||
if (roomSummaries.isNullOrEmpty()) {
|
||||
return RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList()
|
||||
}
|
||||
val mappedRoomSummaries = mapRoomSummaries(roomSummaries)
|
||||
return if (filter.isEmpty()) {
|
||||
mappedRoomSummaries
|
||||
} else {
|
||||
mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) }
|
||||
private fun updateFilteredRoomSummaries(mappedRoomSummaries: ImmutableList<RoomListRoomSummary>, filter: String): ImmutableList<RoomListRoomSummary> {
|
||||
return when {
|
||||
filter.isEmpty() -> emptyList()
|
||||
else -> mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) }
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,10 +26,12 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
data class RoomListState(
|
||||
val matrixUser: MatrixUser?,
|
||||
val roomList: ImmutableList<RoomListRoomSummary>,
|
||||
val filter: String,
|
||||
val filter: String?,
|
||||
val filteredRoomList: ImmutableList<RoomListRoomSummary>,
|
||||
val displayVerificationPrompt: Boolean,
|
||||
val hasNetworkConnection: Boolean,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val displayInvites: Boolean,
|
||||
val displaySearchResults: Boolean,
|
||||
val eventSink: (RoomListEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
|||
aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)),
|
||||
aRoomListState().copy(hasNetworkConnection = false),
|
||||
aRoomListState().copy(displayInvites = true),
|
||||
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
|
||||
aRoomListState().copy(displaySearchResults = true),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -43,10 +45,12 @@ internal fun aRoomListState() = RoomListState(
|
|||
matrixUser = MatrixUser(id = UserId("@id:domain"), username = "User#1", avatarData = AvatarData("@id:domain", "U")),
|
||||
roomList = aRoomListRoomSummaryList(),
|
||||
filter = "filter",
|
||||
filteredRoomList = aRoomListRoomSummaryList(),
|
||||
hasNetworkConnection = true,
|
||||
snackbarMessage = null,
|
||||
displayVerificationPrompt = false,
|
||||
displayInvites = false,
|
||||
displaySearchResults = false,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorVi
|
|||
import io.element.android.features.roomlist.impl.components.RoomListTopBar
|
||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchResultContent
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchResultView
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
|
@ -91,15 +93,27 @@ fun RoomListView(
|
|||
onCreateRoomClicked: () -> Unit = {},
|
||||
onInvitesClicked: () -> Unit = {},
|
||||
) {
|
||||
RoomListContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onCreateRoomClicked = onCreateRoomClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
)
|
||||
Column(modifier = modifier) {
|
||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||
Box {
|
||||
RoomListContent(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onCreateRoomClicked = onCreateRoomClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
)
|
||||
// This overlaid view will only be visible when state.displaySearchResults is true
|
||||
RoomListSearchResultView(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
|
|
@ -163,16 +177,14 @@ fun RoomListContent(
|
|||
Scaffold(
|
||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
Column {
|
||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||
RoomListTopBar(
|
||||
matrixUser = state.matrixUser,
|
||||
filter = state.filter,
|
||||
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
||||
onOpenSettings = onOpenSettings,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
RoomListTopBar(
|
||||
matrixUser = state.matrixUser,
|
||||
areSearchResultsDisplayed = state.displaySearchResults,
|
||||
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
||||
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
|
||||
onOpenSettings = onOpenSettings,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
content = { padding ->
|
||||
Column(
|
||||
|
|
@ -306,7 +318,7 @@ internal fun PreviewRequestVerificationHeaderDark() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun RoomListRoomSummary.contentType() = isPlaceholder
|
||||
internal fun RoomListRoomSummary.contentType() = isPlaceholder
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
|
@ -322,3 +334,11 @@ internal fun RoomListViewDarkPreview(@PreviewParameter(RoomListStateProvider::cl
|
|||
private fun ContentToPreview(state: RoomListState) {
|
||||
RoomListView(state)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListSearchResultContentPreview() {
|
||||
ElementPreviewLight {
|
||||
RoomListSearchResultContent(state = aRoomListState(), onRoomClicked = {})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,57 +20,42 @@ package io.element.android.features.roomlist.impl.components
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.roomlist.impl.R
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomListTopBar(
|
||||
matrixUser: MatrixUser?,
|
||||
filter: String,
|
||||
areSearchResultsDisplayed: Boolean,
|
||||
onFilterChanged: (String) -> Unit,
|
||||
onToggleSearch: () -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -79,124 +64,26 @@ fun RoomListTopBar(
|
|||
tag = "RoomListScreen",
|
||||
msg = "TopBar"
|
||||
)
|
||||
var searchWidgetStateIsOpened by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
fun closeFilter() {
|
||||
onFilterChanged("")
|
||||
searchWidgetStateIsOpened = false
|
||||
}
|
||||
|
||||
BackHandler(enabled = searchWidgetStateIsOpened) {
|
||||
BackHandler(enabled = areSearchResultsDisplayed) {
|
||||
closeFilter()
|
||||
onToggleSearch()
|
||||
}
|
||||
|
||||
if (searchWidgetStateIsOpened) {
|
||||
SearchRoomListTopBar(
|
||||
text = filter,
|
||||
onFilterChanged = onFilterChanged,
|
||||
onCloseClicked = ::closeFilter,
|
||||
scrollBehavior = scrollBehavior,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
DefaultRoomListTopBar(
|
||||
matrixUser = matrixUser,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onSearchClicked = {
|
||||
searchWidgetStateIsOpened = true
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchRoomListTopBar(
|
||||
text: String,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
modifier: Modifier = Modifier,
|
||||
onFilterChanged: (String) -> Unit = {},
|
||||
onCloseClicked: () -> Unit = {},
|
||||
) {
|
||||
var filterState by textFieldState(stateValue = text)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
TopAppBar(
|
||||
modifier = modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
title = {
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
value = filterState,
|
||||
textStyle = TextStyle(
|
||||
fontSize = 17.sp
|
||||
),
|
||||
onValueChange = {
|
||||
filterState = it
|
||||
onFilterChanged(it)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(id = StringR.string.action_search),
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = ContentAlpha.medium)
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (text.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onFilterChanged("")
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "clear",
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onCloseClicked()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "close",
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets(0.dp)
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SearchRoomListTopBarLightPreview() = ElementPreviewLight { SearchRoomListTopBarPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SearchRoomListTopBarDarkPreview() = ElementPreviewDark { SearchRoomListTopBarPreview() }
|
||||
|
||||
@Composable
|
||||
private fun SearchRoomListTopBarPreview() {
|
||||
SearchRoomListTopBar(
|
||||
text = "Hello",
|
||||
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
|
||||
DefaultRoomListTopBar(
|
||||
matrixUser = matrixUser,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onSearchClicked = onToggleSearch,
|
||||
scrollBehavior = scrollBehavior,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun DefaultRoomListTopBar(
|
||||
matrixUser: MatrixUser?,
|
||||
|
|
@ -216,21 +103,19 @@ private fun DefaultRoomListTopBar(
|
|||
},
|
||||
navigationIcon = {
|
||||
if (matrixUser != null) {
|
||||
IconButton(onClick = {}) {
|
||||
Avatar(matrixUser.avatarData)
|
||||
IconButton(
|
||||
modifier = Modifier.testTag(TestTags.homeScreenSettings),
|
||||
onClick = onOpenSettings
|
||||
) {
|
||||
Avatar(matrixUser.avatarData, contentDescription = stringResource(StringR.string.common_settings))
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = onSearchClicked
|
||||
onClick = onSearchClicked,
|
||||
) {
|
||||
Icon(Icons.Default.Search, contentDescription = "search")
|
||||
}
|
||||
IconButton(
|
||||
onClick = onOpenSettings
|
||||
) {
|
||||
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
||||
Icon(Icons.Default.Search, contentDescription = stringResource(StringR.string.action_search))
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
|
|
@ -246,6 +131,7 @@ internal fun DefaultRoomListTopBarLightPreview() = ElementPreviewLight { Default
|
|||
@Composable
|
||||
internal fun DefaultRoomListTopBarDarkPreview() = ElementPreviewDark { DefaultRoomListTopBarPreview() }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun DefaultRoomListTopBarPreview() {
|
||||
DefaultRoomListTopBar(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl.search
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomlist.impl.RoomListEvents
|
||||
import io.element.android.features.roomlist.impl.RoomListState
|
||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||
import io.element.android.features.roomlist.impl.contentType
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.modifiers.applyIf
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.copy
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
|
||||
@Composable
|
||||
internal fun RoomListSearchResultView(
|
||||
state: RoomListState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = state.displaySearchResults,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.applyIf(state.displaySearchResults, ifTrue = {
|
||||
// Disable input interaction to underlying views
|
||||
pointerInput(Unit) {}
|
||||
})
|
||||
) {
|
||||
if (state.displaySearchResults) {
|
||||
RoomListSearchResultContent(state = state, onRoomClicked = onRoomClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun RoomListSearchResultContent(
|
||||
state: RoomListState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val borderColor = MaterialTheme.colorScheme.tertiary
|
||||
val strokeWidth = 1.dp
|
||||
fun onBackButtonPressed() {
|
||||
state.eventSink(RoomListEvents.ToggleSearchResults)
|
||||
}
|
||||
fun onRoomClicked(room: RoomListRoomSummary) {
|
||||
if (room.roomId == null) return
|
||||
onRoomClicked(room.roomId)
|
||||
}
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier.drawBehind {
|
||||
drawLine(
|
||||
color = borderColor,
|
||||
start = Offset(0f, size.height),
|
||||
end = Offset(size.width, size.height),
|
||||
strokeWidth = strokeWidth.value
|
||||
)
|
||||
},
|
||||
navigationIcon = { BackButton(onClick = ::onBackButtonPressed) },
|
||||
title = {
|
||||
val filter = state.filter.orEmpty()
|
||||
val focusRequester = FocusRequester()
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
value = filter,
|
||||
onValueChange = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
containerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
errorIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent
|
||||
),
|
||||
trailingIcon = {
|
||||
if (filter.isNotEmpty()) {
|
||||
IconButton(onClick = {
|
||||
state.eventSink(RoomListEvents.UpdateFilter(""))
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.action_cancel)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(state.displaySearchResults) {
|
||||
if (state.displaySearchResults) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
},
|
||||
windowInsets = TopAppBarDefaults.windowInsets.copy(top = 0)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
val lazyListState = rememberLazyListState()
|
||||
val visibleRange by remember {
|
||||
derivedStateOf {
|
||||
val layoutInfo = lazyListState.layoutInfo
|
||||
val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0
|
||||
val size = layoutInfo.visibleItemsInfo.size
|
||||
firstItemIndex until firstItemIndex + size
|
||||
}
|
||||
}
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override suspend fun onPostFling(
|
||||
consumed: Velocity,
|
||||
available: Velocity
|
||||
): Velocity {
|
||||
state.eventSink(RoomListEvents.UpdateVisibleRange(visibleRange))
|
||||
return super.onPostFling(consumed, available)
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
state = lazyListState,
|
||||
) {
|
||||
items(
|
||||
items = state.filteredRoomList,
|
||||
contentType = { room -> room.contentType() },
|
||||
) { room ->
|
||||
RoomSummaryRow(room = room, onClick = ::onRoomClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_roomlist_main_space_title">"Alle Chats"</string>
|
||||
<string name="state_event_avatar_changed_too">"(Avatar wurde ebenfalls geändert)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s hat seinen Avatar geändert"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Du hast deinen Avatar geändert"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s hat den Anzeigenamen von %2$s in %3$s geändert"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Du hast deinen Anzeigenamen von %1$s in %2$s geändert"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s hat den Anzeigenamen entfernt (war %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Du hast deinen Anzeigenamen entfernt (war %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s hat den Anzeigenamen auf %2$s gesetzt"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Du hast deinen Anzeigenamen auf %1$s gesetzt"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s hat den Raum-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Du hast den Raum-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s hat den Raum-Avatar entfernt"</string>
|
||||
<string name="state_event_room_created">"%1$s hat den Raum erstellt"</string>
|
||||
<string name="state_event_room_created_by_you">"Du hast den Raum erstellt"</string>
|
||||
<string name="state_event_room_invite">"%1$s hat %2$s eingeladen"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s hat die Einladung angenommen"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Du hast die Einladung angenommen"</string>
|
||||
<string name="state_event_room_invite_by_you">"Du hast %1$s eingeladen"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s hat dich eingeladen"</string>
|
||||
<string name="state_event_room_join">"%1$s ist dem Raum beigetreten"</string>
|
||||
<string name="state_event_room_join_by_you">"Du bist dem Raum beigetreten"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s hat deine Beitrittsanfrage abgelehnt"</string>
|
||||
<string name="state_event_room_leave">"%1$s hat den Raum verlassen"</string>
|
||||
<string name="state_event_room_leave_by_you">"Du hast den Raum verlassen"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s hat den Raumnamen geändert in: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Sie haben den Raumnamen geändert in: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s hat den Raumnamen entfernt"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Du hast den Raumnamen entfernt"</string>
|
||||
<string name="state_event_room_reject">"%1$s hat die Einladung abgelehnt"</string>
|
||||
<string name="state_event_room_reject_by_you">"Du hast die Einladung abgelehnt"</string>
|
||||
<string name="state_event_room_remove">"%1$s hat %2$s entfernt"</string>
|
||||
<string name="state_event_room_remove_by_you">"Du hast %1$s entfernt"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s hat das Thema geändert zu: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Sie haben das Thema geändert zu: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s hat das Raumthema entfernt"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Du hast das Raumthema entfernt"</string>
|
||||
</resources>
|
||||
|
|
@ -112,6 +112,8 @@ class RoomListPresenterTests {
|
|||
withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t"))
|
||||
val withFilterState = awaitItem()
|
||||
Truth.assertThat(withFilterState.filter).isEqualTo("t")
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,17 +170,18 @@ class RoomListPresenterTests {
|
|||
val loadedState = awaitItem()
|
||||
// Test filtering with result
|
||||
loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3)))
|
||||
skipItems(1) // Filter update
|
||||
val withNotFilteredRoomState = awaitItem()
|
||||
Truth.assertThat(withNotFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
|
||||
Truth.assertThat(withNotFilteredRoomState.roomList.size).isEqualTo(1)
|
||||
Truth.assertThat(withNotFilteredRoomState.roomList.first())
|
||||
Truth.assertThat(withNotFilteredRoomState.filteredRoomList.size).isEqualTo(1)
|
||||
Truth.assertThat(withNotFilteredRoomState.filteredRoomList.first())
|
||||
.isEqualTo(aRoomListRoomSummary)
|
||||
// Test filtering without result
|
||||
withNotFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
|
||||
skipItems(1) // Filter update
|
||||
val withFilteredRoomState = awaitItem()
|
||||
Truth.assertThat(withFilteredRoomState.filter).isEqualTo("tada")
|
||||
Truth.assertThat(withFilteredRoomState.roomList).isEmpty()
|
||||
Truth.assertThat(withFilteredRoomState.filteredRoomList).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<MatrixUser>
|
||||
suspend fun getProfile(userId: UserId): MatrixUser?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_session_verification_compare_emojis_title">"Emojis vergleichen"</string>
|
||||
<string name="screen_session_verification_open_existing_session_subtitle">"Beweise, dass du es bist, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen."</string>
|
||||
<string name="screen_session_verification_positive_button_initial">"Ich bin bereit"</string>
|
||||
<string name="screen_session_verification_positive_button_verifying_ongoing">"Warten auf Übereinstimmung"</string>
|
||||
<string name="screen_session_verification_they_dont_match">"Sie stimmen nicht überein"</string>
|
||||
<string name="screen_session_verification_they_match">"Sie stimmen überein"</string>
|
||||
<string name="screen_session_verification_cancelled_title">"Verifizierung abgebrochen"</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue