Merge remote-tracking branch 'origin/develop' into local-sdk-fixes
This commit is contained in:
commit
b98b280a3c
343 changed files with 13218 additions and 426 deletions
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
|
|
@ -28,11 +28,6 @@ jobs:
|
|||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
|
|
@ -47,12 +42,14 @@ jobs:
|
|||
app/build/outputs/apk/debug/*.apk
|
||||
- uses: rnkdsh/action-upload-diawi@v1.3.2
|
||||
id: diawi
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
with:
|
||||
env:
|
||||
token: ${{ secrets.DIAWI_TOKEN }}
|
||||
if: ${{ github.event_name == 'pull_request' && env.token != '' }}
|
||||
with:
|
||||
token: ${{ env.token }}
|
||||
file: app/build/outputs/apk/debug/app-arm64-v8a-debug.apk
|
||||
- name: Add or update PR comment with QR Code to download APK.
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
if: ${{ github.event_name == 'pull_request' && steps.diawi.conclusion == 'success' }}
|
||||
uses: NejcZdovc/comment-pr@v2
|
||||
with:
|
||||
message: |
|
||||
|
|
|
|||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
- run: |
|
||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||
- name: Danger
|
||||
uses: danger/danger-js@11.2.4
|
||||
uses: danger/danger-js@11.2.5
|
||||
with:
|
||||
args: "--dangerfile ./tools/danger/dangerfile.js"
|
||||
env:
|
||||
|
|
|
|||
5
.github/workflows/maestro.yml
vendored
5
.github/workflows/maestro.yml
vendored
|
|
@ -24,11 +24,6 @@ jobs:
|
|||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
name: Use JDK 17
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Assemble debug APK
|
||||
run: ./gradlew assembleDebug $CI_GRADLE_ARG_PROPERTIES
|
||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.3.1
|
||||
|
|
|
|||
6
.github/workflows/nightly.yml
vendored
6
.github/workflows/nightly.yml
vendored
|
|
@ -13,13 +13,9 @@ jobs:
|
|||
nightly:
|
||||
name: Build and publish nightly APK to Firebase
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'vector-im/element-x-android' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Install towncrier
|
||||
run: |
|
||||
python3 -m pip install towncrier
|
||||
|
|
|
|||
5
.github/workflows/nightly_manual.yml
vendored
5
.github/workflows/nightly_manual.yml
vendored
|
|
@ -13,11 +13,6 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Install towncrier
|
||||
run: |
|
||||
python3 -m pip install towncrier
|
||||
|
|
|
|||
14
.github/workflows/quality.yml
vendored
14
.github/workflows/quality.yml
vendored
|
|
@ -8,7 +8,7 @@ on:
|
|||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxPermSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
|
||||
|
||||
jobs:
|
||||
|
|
@ -21,11 +21,6 @@ jobs:
|
|||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
|
|
@ -47,7 +42,7 @@ jobs:
|
|||
yarn add danger-plugin-lint-report --dev
|
||||
- name: Danger lint
|
||||
if: always()
|
||||
uses: danger/danger-js@11.2.4
|
||||
uses: danger/danger-js@11.2.5
|
||||
with:
|
||||
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
||||
env:
|
||||
|
|
@ -65,11 +60,6 @@ jobs:
|
|||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
|
|
|
|||
33
.github/workflows/sync-localazy.yml
vendored
Normal file
33
.github/workflows/sync-localazy.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
name: Sync Localazy
|
||||
on:
|
||||
schedule:
|
||||
# At 00:00 on every Monday UTC
|
||||
- cron: '0 0 * * 1'
|
||||
|
||||
jobs:
|
||||
sync-localazy:
|
||||
runs-on: ubuntu-latest
|
||||
# Skip in forks
|
||||
if: github.repository == 'vector-im/element-x-android'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Setup Localazy
|
||||
run: |
|
||||
curl -sS https://dist.localazy.com/debian/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/localazy.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/localazy.gpg] https://maven.localazy.com/repository/apt/ stable main" | sudo tee /etc/apt/sources.list.d/localazy.list
|
||||
sudo apt-get update && sudo apt-get install localazy
|
||||
- name: Run Localazy script
|
||||
run: ./tools/localazy/downloadStrings.sh --all
|
||||
- name: Create Pull Request for Strings
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
commit-message: Sync Strings from Localazy
|
||||
title: Sync Strings
|
||||
body: |
|
||||
- Update Strings from Localazy
|
||||
branch: sync-localazy
|
||||
base: develop
|
||||
5
.github/workflows/tests.yml
vendored
5
.github/workflows/tests.yml
vendored
|
|
@ -25,11 +25,6 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
with:
|
||||
lfs: 'true'
|
||||
- name: ☕️ Use JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
* [Contributing code to Matrix](#contributing-code-to-matrix)
|
||||
* [Android Studio settings](#android-studio-settings)
|
||||
* [Compilation](#compilation)
|
||||
* [I want to help translating Element](#i-want-to-help-translating-element)
|
||||
* [Strings](#strings)
|
||||
* [I want to add new strings to the project](#i-want-to-add-new-strings-to-the-project)
|
||||
* [I want to help translating Element](#i-want-to-help-translating-element)
|
||||
* [I want to submit a PR to fix an issue](#i-want-to-submit-a-pr-to-fix-an-issue)
|
||||
* [Kotlin](#kotlin)
|
||||
* [Changelog](#changelog)
|
||||
|
|
@ -15,7 +17,6 @@
|
|||
* [lint](#lint)
|
||||
* [Unit tests](#unit-tests)
|
||||
* [Tests](#tests)
|
||||
* [Internationalisation](#internationalisation)
|
||||
* [Accessibility](#accessibility)
|
||||
* [Jetpack Compose](#jetpack-compose)
|
||||
* [Authors](#authors)
|
||||
|
|
@ -40,11 +41,28 @@ Please ensure that you're using the project formatting rules (which are in the p
|
|||
|
||||
This project should compile without any special action. Just clone it and open it with Android Studio, or compile from command line using `gradlew`.
|
||||
|
||||
## I want to help translating Element
|
||||
Note: please make sure that the configuration is `app` and not `samples.minimal`.
|
||||
|
||||
For now strings are coming from Element Android project, so:
|
||||
- If you want to fix an issue with an English string, please submit a PR on Element Android.
|
||||
- If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please use [Weblate](https://translate.element.io/projects/element-android/).
|
||||
## Strings
|
||||
|
||||
The strings of the project are managed externally using [https://localazy.com](https://localazy.com) and shared with ElementX iOS.
|
||||
|
||||
### I want to add new strings to the project
|
||||
|
||||
Only the core team can modify or add English strings to Localazy. As an external contributor, if you want to add new strings, feel free to add an Android resource file to the project (for instance a file named `temporary.xml`), with a note in the description of the PR for the reviewer to integrate the String into `Localazy`. If accepted, the reviewer will add the String(s) for you, and then you can download them on your branch (following these [instructions](./tools/localazy/README.md#download-translations)) and remove the temporary file.
|
||||
|
||||
Please follow the naming rules for the key. More details in [the dedicated section in this README.md](./tools/localazy/README.md#key-naming-rules)
|
||||
|
||||
### I want to help translating Element
|
||||
|
||||
Please note that the Localazy project is not open yet for external contributions.
|
||||
|
||||
To help translating, please go to [https://localazy.com/p/element](https://localazy.com/p/element).
|
||||
|
||||
- If you want to fix an issue with an English string, please open an issue on the github project of ElementX (Android or iOS). Only the core team can modify or add English strings.
|
||||
- If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please go to [https://localazy.com/p/element](https://localazy.com/p/element).
|
||||
|
||||
More informations can be found [in this README.md](./tools/localazy/README.md).
|
||||
|
||||
## I want to submit a PR to fix an issue
|
||||
|
||||
|
|
@ -135,10 +153,6 @@ Also, if possible, please test your change on a real device. Testing on Android
|
|||
|
||||
You should consider adding Unit tests with your PR, and also integration tests (AndroidTest). Please refer to [this document](./docs/integration_tests.md) to install and run the integration test environment.
|
||||
|
||||
### Internationalisation
|
||||
|
||||
For now strings are coming from Element Android project, so please read [the documentation](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#internationalisation) from there.
|
||||
|
||||
### Accessibility
|
||||
|
||||
Please consider accessibility as an important point. As a minimum requirement, in layout XML files please use attributes such as `android:contentDescription` and `android:importantForAccessibility`, and test with a screen reader if it's working well. You can add new string resources, dedicated to accessibility, in this case, please prefix theirs id with `a11y_`.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ dependencies {
|
|||
implementation(projects.anvilannotations)
|
||||
api(libs.anvil.compiler.api)
|
||||
implementation(libs.anvil.compiler.utils)
|
||||
implementation("com.squareup:kotlinpoet:1.12.0")
|
||||
implementation("com.squareup:kotlinpoet:1.13.0")
|
||||
implementation(libs.dagger)
|
||||
compileOnly("com.google.auto.service:auto-service-annotations:1.0.1")
|
||||
kapt("com.google.auto.service:auto-service:1.0.1")
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import extension.allServicesImpl
|
|||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
id("io.element.android-compose-application")
|
||||
alias(libs.plugins.stem)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.ksp)
|
||||
|
|
@ -32,6 +33,7 @@ plugins {
|
|||
id("com.google.firebase.appdistribution") version "4.0.0"
|
||||
id("org.jetbrains.kotlinx.knit") version "0.4.0"
|
||||
id("kotlin-parcelize")
|
||||
id("com.google.gms.google-services")
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
@ -139,7 +141,7 @@ android {
|
|||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
// Waiting for https://github.com/google/ksp/issues/37
|
||||
|
|
@ -150,10 +152,6 @@ android {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
|
|
@ -216,15 +214,20 @@ dependencies {
|
|||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
|
||||
implementation(libs.appyx.core)
|
||||
implementation(libs.androidx.splash)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.startup)
|
||||
implementation(libs.androidx.preference)
|
||||
implementation(libs.coil)
|
||||
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
||||
|
||||
implementation(platform(libs.google.firebase.bom))
|
||||
implementation("com.google.firebase:firebase-messaging-ktx")
|
||||
|
||||
implementation(libs.dagger)
|
||||
kapt(libs.dagger.compiler)
|
||||
|
||||
|
|
|
|||
49
app/src/debug/google-services.json
Normal file
49
app/src/debug/google-services.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "912726360885",
|
||||
"firebase_url": "https://vector-alpha.firebaseio.com",
|
||||
"project_id": "vector-alpha",
|
||||
"storage_bucket": "vector-alpha.appspot.com"
|
||||
},
|
||||
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:912726360885:android:def0a4e454042e9b00427c",
|
||||
"android_client_info": {
|
||||
"package_name": "io.element.android.x.debug"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "912726360885-hvgoj23p6plt7hikhtdrakihojghaftv.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "io.element.android.x.debug",
|
||||
"certificate_hash": "41bd63b3b612a15d9ba36a5245c393f2a9b992d1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2022 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
|
@ -31,9 +30,10 @@
|
|||
tools:targetApi="33">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:theme="@style/Theme.ElementX.Splash"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
||||
android:exported="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@style/Theme.ElementX.Splash"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.x
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -30,6 +31,7 @@ import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
|
|||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
||||
import io.element.android.x.di.AppBindings
|
||||
import timber.log.Timber
|
||||
|
||||
class MainActivity : NodeComponentActivity() {
|
||||
|
||||
|
|
@ -52,6 +54,17 @@ class MainActivity : NodeComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when:
|
||||
* - the launcher icon is clicked (if the app is already running);
|
||||
* - a notification is clicked.
|
||||
* - the app is going to background (<- this is strange)
|
||||
*/
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
Timber.w("onNewIntent")
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
bindings<AppBindings>().matrixClientsHolder().onSaveInstanceState(outState)
|
||||
|
|
|
|||
|
|
@ -17,13 +17,18 @@
|
|||
package io.element.android.x.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.DefaultPreferences
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.x.BuildConfig
|
||||
import io.element.android.x.R
|
||||
|
|
@ -46,6 +51,11 @@ object AppModule {
|
|||
return File(context.filesDir, "sessions")
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesResources(@ApplicationContext context: Context): Resources {
|
||||
return context.resources
|
||||
}
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesAppCoroutineScope(): CoroutineScope {
|
||||
|
|
@ -68,6 +78,13 @@ object AppModule {
|
|||
okHttpLoggingLevel = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.BASIC,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
@DefaultPreferences
|
||||
fun providesDefaultSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesCoroutineDispatchers(): CoroutineDispatchers {
|
||||
|
|
@ -78,4 +95,10 @@ object AppModule {
|
|||
diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun provideSnackbarDispatcher(): SnackbarDispatcher {
|
||||
return SnackbarDispatcher()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.x.intent
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.push.impl.intent.IntentProvider
|
||||
import io.element.android.x.MainActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO EAx change to deep-link.
|
||||
@ContributesBinding(AppScope::class)
|
||||
class IntentProviderImpl @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : IntentProvider {
|
||||
override fun getMainIntent(): Intent {
|
||||
return Intent(context, MainActivity::class.java)
|
||||
}
|
||||
|
||||
override fun getIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): Intent {
|
||||
// TODO Handle deeplink or pass parameters
|
||||
return Intent(context, MainActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -37,4 +37,4 @@
|
|||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
40
app/src/release/google-services.json
Normal file
40
app/src/release/google-services.json
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "912726360885",
|
||||
"firebase_url": "https://vector-alpha.firebaseio.com",
|
||||
"project_id": "vector-alpha",
|
||||
"storage_bucket": "vector-alpha.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:912726360885:android:d097de99a4c23d2700427c",
|
||||
"android_client_info": {
|
||||
"package_name": "io.element.android.x"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
|
|
@ -41,10 +41,16 @@ dependencies {
|
|||
allFeaturesApi(rootDir)
|
||||
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.permissions.noop)
|
||||
|
||||
implementation(projects.features.verifysession.api)
|
||||
implementation(projects.features.roomdetails.api)
|
||||
implementation(projects.tests.uitests)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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.appnav
|
||||
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class LoggedInEventProcessor @Inject constructor(
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
roomMembershipObserver: RoomMembershipObserver,
|
||||
sessionVerificationService: SessionVerificationService,
|
||||
) {
|
||||
|
||||
private var observingJob: Job? = null
|
||||
|
||||
private val displayLeftRoomMessage = roomMembershipObserver.updates
|
||||
.map { !it.isUserInRoom }
|
||||
|
||||
private val displayVerificationSuccessfulMessage = sessionVerificationService.verificationFlowState
|
||||
.map { it == VerificationFlowState.Finished }
|
||||
|
||||
fun observeEvents(coroutineScope: CoroutineScope) {
|
||||
observingJob = coroutineScope.launch {
|
||||
displayLeftRoomMessage.onEach {
|
||||
displayMessage(R.string.common_current_user_left_room)
|
||||
}.launchIn(this)
|
||||
|
||||
displayVerificationSuccessfulMessage
|
||||
.drop(1)
|
||||
.onEach {
|
||||
displayMessage(R.string.common_verification_complete)
|
||||
}.launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopObserving() {
|
||||
observingJob?.cancel()
|
||||
observingJob = null
|
||||
}
|
||||
|
||||
private suspend fun displayMessage(message: Int) {
|
||||
snackbarDispatcher.post(SnackbarMessage(message))
|
||||
}
|
||||
}
|
||||
|
|
@ -32,9 +32,11 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.appnav.loggedin.LoggedInNode
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||
|
|
@ -46,13 +48,18 @@ import io.element.android.libraries.architecture.bindings
|
|||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class LoggedInFlowNode @AssistedInject constructor(
|
||||
|
|
@ -63,6 +70,8 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
private val verifySessionEntryPoint: VerifySessionEntryPoint,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
snackbarDispatcher: SnackbarDispatcher,
|
||||
) : BackstackNode<LoggedInFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.RoomList,
|
||||
|
|
@ -87,6 +96,11 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
||||
snackbarDispatcher,
|
||||
inputs.matrixClient.roomMembershipObserver(),
|
||||
inputs.matrixClient.sessionVerificationService(),
|
||||
)
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
|
|
@ -99,6 +113,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
appNavigationStateService.onNavigateToSession(inputs.matrixClient.sessionId)
|
||||
// TODO We do not support Space yet, so directly navigate to main space
|
||||
appNavigationStateService.onNavigateToSpace(MAIN_SPACE)
|
||||
loggedInFlowProcessor.observeEvents(coroutineScope)
|
||||
},
|
||||
onDestroy = {
|
||||
val imageLoaderFactory = bindings<MatrixUIBindings>().notLoggedInImageLoaderFactory()
|
||||
|
|
@ -106,11 +121,15 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
plugins<LifecycleCallback>().forEach { it.onFlowReleased(inputs.matrixClient) }
|
||||
appNavigationStateService.onLeavingSpace()
|
||||
appNavigationStateService.onLeavingSession()
|
||||
loggedInFlowProcessor.stopObserving()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Permanent : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object RoomList : NavTarget
|
||||
|
||||
|
|
@ -129,6 +148,9 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Permanent -> {
|
||||
createNode<LoggedInNode>(buildContext)
|
||||
}
|
||||
NavTarget.RoomList -> {
|
||||
val callback = object : RoomListEntryPoint.Callback {
|
||||
override fun onRoomClicked(roomId: RoomId) {
|
||||
|
|
@ -178,7 +200,16 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
.build()
|
||||
}
|
||||
NavTarget.CreateRoom -> {
|
||||
createRoomEntryPoint.createNode(this, buildContext)
|
||||
val callback = object : CreateRoomEntryPoint.Callback {
|
||||
override fun onOpenRoom(roomId: RoomId) {
|
||||
backstack.replace(NavTarget.Room(roomId))
|
||||
}
|
||||
}
|
||||
|
||||
createRoomEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
.callback(callback)
|
||||
.build()
|
||||
}
|
||||
NavTarget.VerifySession -> {
|
||||
verifySessionEntryPoint.createNode(this, buildContext)
|
||||
|
|
@ -188,11 +219,15 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
// Animate navigation to settings and to a room
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
Box(modifier = modifier) {
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = Modifier,
|
||||
// Animate navigation to settings and to a room
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
|
||||
PermanentChild(navTarget = NavTarget.Permanent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
|
|
@ -38,7 +39,12 @@ import io.element.android.libraries.architecture.animation.rememberDefaultTransi
|
|||
import io.element.android.libraries.architecture.inputs
|
||||
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
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -49,6 +55,8 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
private val messagesEntryPoint: MessagesEntryPoint,
|
||||
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
roomMembershipObserver: RoomMembershipObserver,
|
||||
coroutineScope: CoroutineScope,
|
||||
) : BackstackNode<RoomFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Messages,
|
||||
|
|
@ -68,6 +76,9 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val timeline = inputs.room.timeline()
|
||||
|
||||
private val roomFlowPresenter = RoomFlowPresenter(inputs.room)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
|
|
@ -83,6 +94,13 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
appNavigationStateService.onLeavingRoom()
|
||||
}
|
||||
)
|
||||
|
||||
roomMembershipObserver.updates
|
||||
.filter { update -> update.roomId == inputs.room.roomId && !update.isUserInRoom }
|
||||
.onEach {
|
||||
navigateUp()
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -95,7 +113,7 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
})
|
||||
}
|
||||
NavTarget.RoomDetails -> {
|
||||
roomDetailsEntryPoint.createNode(this, buildContext)
|
||||
roomDetailsEntryPoint.createNode(this, buildContext, emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,6 +128,7 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
roomFlowPresenter.present()
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.appnav
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import timber.log.Timber
|
||||
|
||||
class RoomFlowPresenter(
|
||||
private val room: MatrixRoom,
|
||||
) : Presenter<RoomFlowState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomFlowState {
|
||||
// Preload room members so we can quickly detect if the room is a DM room
|
||||
LaunchedEffect(Unit) {
|
||||
room.fetchMembers()
|
||||
.onFailure {
|
||||
Timber.e(it, "Fail to fetch members for room ${room.roomId}")
|
||||
}.onSuccess {
|
||||
Timber.v("Success fetching members for room ${room.roomId}")
|
||||
}
|
||||
}
|
||||
|
||||
return RoomFlowState
|
||||
}
|
||||
}
|
||||
|
||||
// At first the return type was Unit, but detekt complained about it
|
||||
object RoomFlowState
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.appnav.loggedin
|
||||
|
||||
// sealed interface LoggedInEvents {
|
||||
// object MyEvent : LoggedInEvents
|
||||
// }
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.appnav.loggedin
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class LoggedInNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val loggedInPresenter: LoggedInPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val loggedInState = loggedInPresenter.present()
|
||||
LoggedInView(
|
||||
state = loggedInState,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.appnav.loggedin
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import javax.inject.Inject
|
||||
|
||||
class LoggedInPresenter @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
private val pushService: PushService,
|
||||
) : Presenter<LoggedInState> {
|
||||
|
||||
private val postNotificationPermissionsPresenter by lazy {
|
||||
// Ask for POST_NOTIFICATION PERMISSION on Android 13+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
NoopPermissionsPresenter()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): LoggedInState {
|
||||
LaunchedEffect(Unit) {
|
||||
// Ensure pusher is registered
|
||||
pushService.registerFirebasePusher(matrixClient)
|
||||
}
|
||||
|
||||
val permissionsState = postNotificationPermissionsPresenter.present()
|
||||
|
||||
// fun handleEvents(event: LoggedInEvents) {
|
||||
// when (event) {
|
||||
// }
|
||||
// }
|
||||
|
||||
return LoggedInState(
|
||||
permissionsState = permissionsState,
|
||||
// eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.appnav.loggedin
|
||||
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
|
||||
data class LoggedInState(
|
||||
val permissionsState: PermissionsState,
|
||||
// val eventSink: (LoggedInEvents) -> Unit
|
||||
)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.appnav.loggedin
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState
|
||||
|
||||
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
||||
override val values: Sequence<LoggedInState>
|
||||
get() = sequenceOf(
|
||||
aLoggedInState(),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
||||
fun aLoggedInState() = LoggedInState(
|
||||
permissionsState = createDummyPostNotificationPermissionsState(),
|
||||
// eventSink = {}
|
||||
)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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.appnav.loggedin
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.androidutils.system.openAppSettingsPage
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.permissions.api.PermissionsView
|
||||
|
||||
@Composable
|
||||
fun LoggedInView(
|
||||
state: LoggedInState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val activity = LocalContext.current as? Activity
|
||||
|
||||
PermissionsView(
|
||||
state = state.permissionsState,
|
||||
modifier = modifier,
|
||||
openSystemSettings = {
|
||||
activity?.let { openAppSettingsPage(it, "") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoggedInViewLightPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoggedInViewDarkPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: LoggedInState) {
|
||||
LoggedInView(
|
||||
state = state
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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.appnav
|
||||
|
||||
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.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
class RoomFlowPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - fetches room members`() = runTest {
|
||||
val fakeTimeline = FakeMatrixTimeline()
|
||||
val room = FakeMatrixRoom(matrixTimeline = fakeTimeline)
|
||||
val presenter = RoomFlowPresenter(room)
|
||||
|
||||
Truth.assertThat(room.areMembersFetched).isFalse()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
Truth.assertThat(room.areMembersFetched).isTrue()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - recovers from error while fetching room members`() = runTest {
|
||||
val fakeTimeline = FakeMatrixTimeline()
|
||||
val room = FakeMatrixRoom(matrixTimeline = fakeTimeline).apply {
|
||||
givenFetchMemberResult(Result.failure(IllegalStateException("Some error")))
|
||||
}
|
||||
val presenter = RoomFlowPresenter(room)
|
||||
|
||||
Truth.assertThat(room.areMembersFetched).isFalse()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
Truth.assertThat(room.areMembersFetched).isFalse()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.appnav.loggedin
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class LoggedInPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.permissionsState.permission).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(): LoggedInPresenter {
|
||||
return LoggedInPresenter(
|
||||
matrixClient = FakeMatrixClient(),
|
||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||
override fun create(permission: String): PermissionsPresenter {
|
||||
return NoopPermissionsPresenter()
|
||||
}
|
||||
},
|
||||
pushService = object : PushService {
|
||||
override fun notificationStyleChanged() {
|
||||
}
|
||||
|
||||
override suspend fun registerFirebasePusher(matrixClient: MatrixClient) {
|
||||
}
|
||||
|
||||
override suspend fun testPush() {
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
|||
buildscript {
|
||||
dependencies {
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
|
||||
classpath("com.google.gms:google-services:4.3.15")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,6 +231,8 @@ koverMerged {
|
|||
overrideClassFilter {
|
||||
includes += "*Presenter"
|
||||
excludes += "*TemplatePresenter"
|
||||
excludes += "*Fake*Presenter"
|
||||
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
|
||||
}
|
||||
bound {
|
||||
minValue = 90
|
||||
|
|
@ -246,6 +249,7 @@ koverMerged {
|
|||
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*"
|
||||
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*"
|
||||
excludes += "io.element.android.libraries.matrix.api.room.RoomMembershipState*"
|
||||
excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*"
|
||||
}
|
||||
bound {
|
||||
minValue = 90
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
Implement Room Details screen
|
||||
Implement Room Member List screen
|
||||
|
|
|
|||
1
changelog.d/286.feature
Normal file
1
changelog.d/286.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Add leave room functionality to the Room Details screen.
|
||||
1
changelog.d/96.feature
Normal file
1
changelog.d/96.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
[Create and join rooms] Show or create direct message room
|
||||
|
|
@ -251,7 +251,8 @@ Main libraries and frameworks used in this application:
|
|||
|
||||
- Navigation state with [Appyx](https://bumble-tech.github.io/appyx/). Please
|
||||
watch [this video](https://www.droidcon.com/2022/11/15/model-driven-navigation-with-appyx-from-zero-to-hero/) to learn more about Appyx!
|
||||
- DI: [Dagger](https://dagger.dev/) and [Anvil](https://github.com/square/anvil)
|
||||
- DI: [Dagger](https://dagger.dev/) and [Anvil](https://github.com/square/anvil). Please
|
||||
watch [this video](https://www.droidcon.com/2022/06/28/dagger-anvil-learning-to-love-dependency-injection/) to learn more about Anvil!
|
||||
- Reactive State management with Compose runtime and [Molecule](https://github.com/cashapp/molecule)
|
||||
|
||||
Some patterns are inspired by [Circuit](https://slackhq.github.io/circuit/)
|
||||
|
|
|
|||
|
|
@ -24,4 +24,5 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,21 @@
|
|||
|
||||
package io.element.android.features.createroom.api
|
||||
|
||||
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface CreateRoomEntryPoint : SimpleFeatureEntryPoint
|
||||
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
||||
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onOpenRoom(roomId: RoomId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ dependencies {
|
|||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.features.selectusers.api)
|
||||
implementation(projects.features.userlist.api)
|
||||
api(projects.features.createroom.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
|
|
@ -56,7 +56,8 @@ dependencies {
|
|||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.selectusers.impl)
|
||||
testImplementation(projects.features.userlist.impl)
|
||||
testImplementation(projects.features.userlist.test)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.createroom.impl
|
||||
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO this is empty as we currently don't have an endpoint to perform user search
|
||||
class AllMatrixUsersDataSource @Inject constructor() : MatrixUserDataSource {
|
||||
override suspend fun search(query: String): List<MatrixUser> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun getProfile(userId: UserId): MatrixUser? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -23,17 +23,20 @@ import com.bumble.appyx.core.composable.Children
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
|
||||
import io.element.android.features.createroom.impl.root.CreateRoomRootNode
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
|
|
@ -64,6 +67,10 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
|||
override fun onCreateNewRoom() {
|
||||
backstack.push(NavTarget.NewRoom)
|
||||
}
|
||||
|
||||
override fun onOpenRoom(roomId: RoomId) {
|
||||
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) }
|
||||
}
|
||||
}
|
||||
createNode<CreateRoomRootNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.createroom.impl
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -26,7 +27,21 @@ import javax.inject.Inject
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<CreateRoomFlowNode>(buildContext)
|
||||
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder {
|
||||
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : CreateRoomEntryPoint.NodeBuilder {
|
||||
|
||||
override fun callback(callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<CreateRoomFlowNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,31 +17,38 @@
|
|||
package io.element.android.features.createroom.impl.addpeople
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.selectusers.api.SelectUsersPresenter
|
||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
||||
import io.element.android.features.selectusers.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class AddPeoplePresenter @Inject constructor(
|
||||
private val selectUsersPresenterFactory: SelectUsersPresenter.Factory,
|
||||
private val userListPresenterFactory: UserListPresenter.Factory,
|
||||
@Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource,
|
||||
) : Presenter<AddPeopleState> {
|
||||
|
||||
private val selectUsersPresenter by lazy {
|
||||
selectUsersPresenterFactory.create(SelectUsersPresenterArgs(SelectionMode.Multiple))
|
||||
private val userListPresenter by lazy {
|
||||
userListPresenterFactory.create(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
||||
matrixUserDataSource,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): AddPeopleState {
|
||||
val selectUsersState = selectUsersPresenter.present()
|
||||
val userListState = userListPresenter.present()
|
||||
|
||||
fun handleEvents(event: AddPeopleEvents) {
|
||||
// do nothing for now
|
||||
}
|
||||
|
||||
return AddPeopleState(
|
||||
selectUsersState = selectUsersState,
|
||||
userListState = userListState,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.addpeople
|
||||
|
||||
import io.element.android.features.selectusers.api.SelectUsersState
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
|
||||
data class AddPeopleState(
|
||||
val selectUsersState: SelectUsersState,
|
||||
val userListState: UserListState,
|
||||
val eventSink: (AddPeopleEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,22 +17,22 @@
|
|||
package io.element.android.features.createroom.impl.addpeople
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.selectusers.api.SelectionMode
|
||||
import io.element.android.features.selectusers.api.aListOfSelectedUsers
|
||||
import io.element.android.features.selectusers.api.aSelectUsersState
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
|
||||
open class AddPeopleStateProvider : PreviewParameterProvider<AddPeopleState> {
|
||||
override val values: Sequence<AddPeopleState>
|
||||
get() = sequenceOf(
|
||||
aAddPeopleState(),
|
||||
aAddPeopleState().copy(
|
||||
selectUsersState = aSelectUsersState().copy(
|
||||
userListState = aUserListState().copy(
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
)
|
||||
),
|
||||
aAddPeopleState().copy(
|
||||
selectUsersState = aSelectUsersState().copy(
|
||||
userListState = aUserListState().copy(
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
isSearchActive = true,
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
|
|
@ -42,6 +42,6 @@ open class AddPeopleStateProvider : PreviewParameterProvider<AddPeopleState> {
|
|||
}
|
||||
|
||||
fun aAddPeopleState() = AddPeopleState(
|
||||
selectUsersState = aSelectUsersState(),
|
||||
userListState = aUserListState(),
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.userlist.api.UserListView
|
||||
import io.element.android.features.createroom.impl.R
|
||||
import io.element.android.features.selectusers.api.SelectUsersView
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
|
@ -52,9 +52,9 @@ fun AddPeopleView(
|
|||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (!state.selectUsersState.isSearchActive) {
|
||||
if (!state.userListState.isSearchActive) {
|
||||
AddPeopleViewTopBar(
|
||||
hasSelectedUsers = state.selectUsersState.selectedUsers.isNotEmpty(),
|
||||
hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(),
|
||||
onBackPressed = onBackPressed,
|
||||
onNextPressed = onNextPressed,
|
||||
)
|
||||
|
|
@ -66,9 +66,9 @@ fun AddPeopleView(
|
|||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
) {
|
||||
SelectUsersView(
|
||||
UserListView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.selectUsersState,
|
||||
state = state.userListState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import io.element.android.features.createroom.impl.AllMatrixUsersDataSource
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Named
|
||||
|
||||
@Module
|
||||
@ContributesTo(AppScope::class)
|
||||
interface CreateRoomModule {
|
||||
|
||||
@Binds
|
||||
@Named("AllUsers")
|
||||
fun bindAllUserListDataSource(dataSource: AllMatrixUsersDataSource): MatrixUserDataSource
|
||||
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.createroom.impl.root
|
|||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
|
||||
sealed interface CreateRoomRootEvents {
|
||||
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
|
||||
object InvitePeople : CreateRoomRootEvents
|
||||
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
|
||||
object CancelStartDM : CreateRoomRootEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
|
|
@ -27,7 +26,7 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class CreateRoomRootNode @AssistedInject constructor(
|
||||
|
|
@ -38,15 +37,17 @@ class CreateRoomRootNode @AssistedInject constructor(
|
|||
|
||||
interface Callback : Plugin {
|
||||
fun onCreateNewRoom()
|
||||
fun onOpenRoom(roomId: RoomId)
|
||||
}
|
||||
|
||||
private fun onCreateNewRoom() {
|
||||
plugins<Callback>().forEach { it.onCreateNewRoom() }
|
||||
}
|
||||
private val callback = object : Callback {
|
||||
override fun onCreateNewRoom() {
|
||||
plugins<Callback>().forEach { it.onCreateNewRoom() }
|
||||
}
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Root : NavTarget
|
||||
override fun onOpenRoom(roomId: RoomId) {
|
||||
plugins<Callback>().forEach { it.onOpenRoom(roomId) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -56,7 +57,8 @@ class CreateRoomRootNode @AssistedInject constructor(
|
|||
state = state,
|
||||
modifier = modifier,
|
||||
onClosePressed = this::navigateUp,
|
||||
onNewRoomClicked = this::onCreateNewRoom,
|
||||
onNewRoomClicked = callback::onCreateNewRoom,
|
||||
onOpenDM = callback::onOpenRoom,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,40 +17,73 @@
|
|||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.selectusers.api.SelectUsersPresenter
|
||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
||||
import io.element.android.features.selectusers.api.SelectionMode
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
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.architecture.execute
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import timber.log.Timber
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class CreateRoomRootPresenter @Inject constructor(
|
||||
private val presenterFactory: SelectUsersPresenter.Factory,
|
||||
private val presenterFactory: UserListPresenter.Factory,
|
||||
@Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource,
|
||||
private val matrixClient: MatrixClient,
|
||||
) : Presenter<CreateRoomRootState> {
|
||||
|
||||
private val presenter by lazy {
|
||||
presenterFactory.create(SelectUsersPresenterArgs(SelectionMode.Single))
|
||||
presenterFactory.create(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
matrixUserDataSource,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): CreateRoomRootState {
|
||||
val selectUsersState = presenter.present()
|
||||
val userListState = presenter.present()
|
||||
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val startDmAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
|
||||
fun startDm(matrixUser: MatrixUser) {
|
||||
startDmAction.value = Async.Uninitialized
|
||||
val existingDM = matrixClient.findDM(matrixUser.id)
|
||||
if (existingDM == null) {
|
||||
localCoroutineScope.createDM(matrixUser, startDmAction)
|
||||
} else {
|
||||
startDmAction.value = Async.Success(existingDM.roomId)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: CreateRoomRootEvents) {
|
||||
when (event) {
|
||||
is CreateRoomRootEvents.StartDM -> handleStartDM(event.matrixUser)
|
||||
is CreateRoomRootEvents.StartDM -> startDm(event.matrixUser)
|
||||
CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized
|
||||
CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action
|
||||
}
|
||||
}
|
||||
|
||||
return CreateRoomRootState(
|
||||
selectUsersState = selectUsersState,
|
||||
userListState = userListState,
|
||||
startDmAction = startDmAction.value,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleStartDM(matrixUser: MatrixUser) {
|
||||
Timber.d("handleStartDM: $matrixUser") // Todo handle start DM action
|
||||
private fun CoroutineScope.createDM(user: MatrixUser, startDmAction: MutableState<Async<RoomId>>) = launch {
|
||||
suspend {
|
||||
matrixClient.createDM(user.id).getOrThrow()
|
||||
}.execute(startDmAction)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,12 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import io.element.android.features.selectusers.api.SelectUsersState
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
data class CreateRoomRootState(
|
||||
val selectUsersState: SelectUsersState,
|
||||
val userListState: UserListState,
|
||||
val startDmAction: Async<RoomId>,
|
||||
val eventSink: (CreateRoomRootEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,16 +17,42 @@
|
|||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.selectusers.api.aSelectUsersState
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRootState> {
|
||||
override val values: Sequence<CreateRoomRootState>
|
||||
get() = sequenceOf(
|
||||
aCreateRoomRootState(),
|
||||
aCreateRoomRootState().copy(
|
||||
startDmAction = Async.Loading(),
|
||||
userListState = aMatrixUser().let {
|
||||
aUserListState().copy(
|
||||
searchQuery = it.id.value,
|
||||
searchResults = persistentListOf(it),
|
||||
selectedUsers = persistentListOf(it),
|
||||
isSearchActive = true,
|
||||
)
|
||||
}
|
||||
),
|
||||
aCreateRoomRootState().copy(
|
||||
startDmAction = Async.Failure(Throwable()),
|
||||
userListState = aMatrixUser().let {
|
||||
aUserListState().copy(
|
||||
searchQuery = it.id.value,
|
||||
searchResults = persistentListOf(it),
|
||||
selectedUsers = persistentListOf(it),
|
||||
isSearchActive = true,
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aCreateRoomRootState() = CreateRoomRootState(
|
||||
eventSink = {},
|
||||
selectUsersState = aSelectUsersState(),
|
||||
startDmAction = Async.Uninitialized,
|
||||
userListState = aUserListState(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -28,9 +29,11 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -38,7 +41,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.createroom.impl.R
|
||||
import io.element.android.features.selectusers.api.SelectUsersView
|
||||
import io.element.android.features.userlist.api.UserListView
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
||||
|
|
@ -46,6 +52,7 @@ 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.Text
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.designsystem.R as DrawableR
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
|
|
@ -56,11 +63,18 @@ fun CreateRoomRootView(
|
|||
modifier: Modifier = Modifier,
|
||||
onClosePressed: () -> Unit = {},
|
||||
onNewRoomClicked: () -> Unit = {},
|
||||
onOpenDM: (RoomId) -> Unit = {},
|
||||
) {
|
||||
if (state.startDmAction is Async.Success) {
|
||||
LaunchedEffect(state.startDmAction) {
|
||||
onOpenDM(state.startDmAction.state)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
topBar = {
|
||||
if (!state.selectUsersState.isSearchActive) {
|
||||
if (!state.userListState.isSearchActive) {
|
||||
CreateRoomRootViewTopBar(onClosePressed = onClosePressed)
|
||||
}
|
||||
}
|
||||
|
|
@ -69,13 +83,19 @@ fun CreateRoomRootView(
|
|||
modifier = Modifier.padding(paddingValues),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
SelectUsersView(
|
||||
val context = LocalContext.current
|
||||
UserListView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.selectUsersState,
|
||||
onUserSelected = { state.eventSink.invoke(CreateRoomRootEvents.StartDM(it)) },
|
||||
state = state.userListState,
|
||||
onUserSelected = {
|
||||
// Fixme disabled DM creation since it can break the account data which is not correctly synced
|
||||
// uncomment to enable it again or move behind a feature flag
|
||||
Toast.makeText(context, "Create DM feature is disabled.", Toast.LENGTH_SHORT).show()
|
||||
// state.eventSink(CreateRoomRootEvents.StartDM(it))
|
||||
},
|
||||
)
|
||||
|
||||
if (!state.selectUsersState.isSearchActive) {
|
||||
if (!state.userListState.isSearchActive) {
|
||||
CreateRoomActionButtonsList(
|
||||
onNewRoomClicked = onNewRoomClicked,
|
||||
onInvitePeopleClicked = { state.eventSink(CreateRoomRootEvents.InvitePeople) },
|
||||
|
|
@ -83,6 +103,25 @@ fun CreateRoomRootView(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (state.startDmAction) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog(text = stringResource(id = StringR.string.common_creating_room))
|
||||
}
|
||||
is Async.Failure -> {
|
||||
RetryDialog(
|
||||
content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat),
|
||||
onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) },
|
||||
onRetry = {
|
||||
state.userListState.selectedUsers.firstOrNull()
|
||||
?.let { state.eventSink(CreateRoomRootEvents.StartDM(it)) }
|
||||
// Cancel start DM if there is no more selected user (should not happen)
|
||||
?: state.eventSink(CreateRoomRootEvents.CancelStartDM)
|
||||
},
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
|
|||
|
|
@ -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_create_room_action_create_room">"Nueva sala"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Invitar gente"</string>
|
||||
<string name="screen_create_room_add_people_title">"Añadir personas"</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_create_room_action_create_room">"Nuova stanza"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Invita persone"</string>
|
||||
<string name="screen_create_room_add_people_title">"Aggiungi persone"</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_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>
|
||||
</resources>
|
||||
|
|
@ -22,8 +22,8 @@ import app.cash.molecule.RecompositionClock
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
||||
import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter
|
||||
import io.element.android.features.userlist.test.FakeMatrixUserDataSource
|
||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
|
|
@ -35,10 +35,7 @@ class AddPeoplePresenterTests {
|
|||
|
||||
@Before
|
||||
fun setup() {
|
||||
val selectUsersFactory = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory {
|
||||
override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args)
|
||||
}
|
||||
presenter = AddPeoplePresenter(selectUsersFactory)
|
||||
presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeMatrixUserDataSource())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -51,3 +48,4 @@ class AddPeoplePresenterTests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,18 @@ import app.cash.molecule.RecompositionClock
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
||||
import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
import io.element.android.features.userlist.test.FakeMatrixUserDataSource
|
||||
import io.element.android.features.userlist.test.FakeUserListPresenter
|
||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
|
|
@ -33,14 +41,17 @@ import org.junit.Test
|
|||
|
||||
class CreateRoomRootPresenterTests {
|
||||
|
||||
private lateinit var userListDataSource: FakeMatrixUserDataSource
|
||||
private lateinit var presenter: CreateRoomRootPresenter
|
||||
private lateinit var fakeUserListPresenter: FakeUserListPresenter
|
||||
private lateinit var fakeMatrixClient: FakeMatrixClient
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val selectUsersPresenter = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory {
|
||||
override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args)
|
||||
}
|
||||
presenter = CreateRoomRootPresenter(selectUsersPresenter)
|
||||
fakeUserListPresenter = FakeUserListPresenter()
|
||||
fakeMatrixClient = FakeMatrixClient()
|
||||
userListDataSource = FakeMatrixUserDataSource()
|
||||
presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userListDataSource, fakeMatrixClient)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -64,13 +75,82 @@ class CreateRoomRootPresenterTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - trigger start DM action`() = runTest {
|
||||
fun `present - trigger create DM action`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val matrixUser = MatrixUser(UserId("@name:matrix.org"))
|
||||
val createDmResult = Result.success(RoomId("!createDmResult"))
|
||||
|
||||
fakeMatrixClient.givenFindDmResult(null)
|
||||
fakeMatrixClient.givenCreateDmResult(createDmResult)
|
||||
|
||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
|
||||
val stateAfterStartDM = awaitItem()
|
||||
assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - trigger retrieve DM action`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val matrixUser = MatrixUser(UserId("@name:matrix.org"))
|
||||
val fakeDmResult = FakeMatrixRoom(RoomId("!fakeDmResult"))
|
||||
|
||||
fakeMatrixClient.givenFindDmResult(fakeDmResult)
|
||||
|
||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
val stateAfterStartDM = awaitItem()
|
||||
assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(fakeDmResult.roomId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - trigger retry create DM action`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val matrixUser = MatrixUser(UserId("@name:matrix.org"))
|
||||
val createDmResult = Result.success(RoomId("!createDmResult"))
|
||||
fakeUserListPresenter.givenState(aUserListState().copy(selectedUsers = persistentListOf(matrixUser)))
|
||||
|
||||
fakeMatrixClient.givenFindDmResult(null)
|
||||
fakeMatrixClient.givenCreateDmError(A_THROWABLE)
|
||||
fakeMatrixClient.givenCreateDmResult(createDmResult)
|
||||
|
||||
// Failure
|
||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
|
||||
val stateAfterStartDM = awaitItem()
|
||||
assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Failure::class.java)
|
||||
|
||||
// Cancel
|
||||
stateAfterStartDM.eventSink(CreateRoomRootEvents.CancelStartDM)
|
||||
val stateAfterCancel = awaitItem()
|
||||
assertThat(stateAfterCancel.startDmAction).isInstanceOf(Async.Uninitialized::class.java)
|
||||
|
||||
// Failure
|
||||
stateAfterCancel.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
|
||||
val stateAfterSecondAttempt = awaitItem()
|
||||
assertThat(stateAfterSecondAttempt.startDmAction).isInstanceOf(Async.Failure::class.java)
|
||||
|
||||
// Retry with success
|
||||
fakeMatrixClient.givenCreateDmError(null)
|
||||
stateAfterSecondAttempt.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Uninitialized::class.java)
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
|
||||
val stateAfterRetryStartDM = awaitItem()
|
||||
assertThat(stateAfterRetryStartDM.startDmAction).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(stateAfterRetryStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
|
|
@ -77,6 +79,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.autofill
|
||||
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
|
|
@ -206,6 +209,7 @@ internal fun ChangeServerSection(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
internal fun LoginForm(
|
||||
state: LoginRootState,
|
||||
|
|
@ -239,7 +243,11 @@ internal fun LoginForm(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onTabOrEnterKeyFocusNext(focusManager)
|
||||
.testTag(TestTags.loginEmailUsername),
|
||||
.testTag(TestTags.loginEmailUsername)
|
||||
.autofill(autofillTypes = listOf(AutofillType.Username), onFill = {
|
||||
loginFieldState = it
|
||||
eventSink(LoginRootEvents.SetLogin(it))
|
||||
}),
|
||||
label = {
|
||||
Text(text = stringResource(R.string.screen_login_username_hint))
|
||||
},
|
||||
|
|
@ -279,7 +287,11 @@ internal fun LoginForm(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onTabOrEnterKeyFocusNext(focusManager)
|
||||
.testTag(TestTags.loginPassword),
|
||||
.testTag(TestTags.loginPassword)
|
||||
.autofill(autofillTypes = listOf(AutofillType.Password), onFill = {
|
||||
passwordFieldState = it
|
||||
eventSink(LoginRootEvents.SetPassword(it))
|
||||
}),
|
||||
onValueChange = {
|
||||
passwordFieldState = it
|
||||
eventSink(LoginRootEvents.SetPassword(it))
|
||||
|
|
|
|||
20
features/login/impl/src/main/res/values-es/translations.xml
Normal file
20
features/login/impl/src/main/res/values-es/translations.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_change_server_error_invalid_homeserver">"No hemos podido acceder a este servidor. Comprueba que has introducido correctamente la dirección del servidor. Si la dirección es correcta, ponte en contacto con el administrador del servidor para obtener más ayuda."</string>
|
||||
<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_login_username_hint">"Usuario"</string>
|
||||
</resources>
|
||||
20
features/login/impl/src/main/res/values-it/translations.xml
Normal file
20
features/login/impl/src/main/res/values-it/translations.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Non siamo riusciti a raggiungere questo homserver. Verifica di aver inserito correttamente l\'URL del server domestico. Se l\'URL è corretto, contatta l\'amministratore del tuo server domestico per ulteriore assistenza."</string>
|
||||
<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_login_username_hint">"Nome utente"</string>
|
||||
</resources>
|
||||
20
features/login/impl/src/main/res/values-ro/translations.xml
Normal file
20
features/login/impl/src/main/res/values-ro/translations.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Nu am putut accesa acest homeserver. Te rugăm să verifici că ai introdus corect adresa URL a homeserver-ului. Dacă adresa URL este corectă, contactează administratorul homeserver-ului pentru ajutor suplimentar."</string>
|
||||
<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_login_username_hint">"Utilizator"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_signout_confirmation_dialog_content">"¿Estás seguro de que quieres cerrar sesión?"</string>
|
||||
<string name="screen_signout_confirmation_dialog_submit">"Cerrar sesión"</string>
|
||||
<string name="screen_signout_confirmation_dialog_title">"Cerrar sesión"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"Cerrando sesión…"</string>
|
||||
<string name="screen_signout_preference_item">"Cerrar sesión"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_signout_confirmation_dialog_content">"Sei sicuro di voler uscire?"</string>
|
||||
<string name="screen_signout_confirmation_dialog_submit">"Esci"</string>
|
||||
<string name="screen_signout_confirmation_dialog_title">"Esci"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"Uscita in corso…"</string>
|
||||
<string name="screen_signout_preference_item">"Esci"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_signout_confirmation_dialog_content">"Sunteți sigur că vreți să vă deconectați?"</string>
|
||||
<string name="screen_signout_confirmation_dialog_submit">"Deconectați-vă"</string>
|
||||
<string name="screen_signout_confirmation_dialog_title">"Deconectați-vă"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"Deconectare în curs…"</string>
|
||||
<string name="screen_signout_preference_item">"Deconectați-vă"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_onboarding_welcome_subtitle">"Bienvenido a la beta de %1$s. Vitaminado, para mayor rapidez y sencillez."</string>
|
||||
<string name="screen_onboarding_welcome_title">"Siéntente en tu Elemento"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_onboarding_welcome_subtitle">"Benvenuto nella beta di %1$s. Potenziato in velocità e semplicità."</string>
|
||||
<string name="screen_onboarding_welcome_title">"Sii nel tuo elemento"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_onboarding_welcome_subtitle">"Bun venit la versiunea beta a %1$s. Supraalimentat, pentru viteză și simplitate."</string>
|
||||
<string name="screen_onboarding_welcome_title">"Fii în Elementul tău"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="crash_detection_dialog_content">"%1$s se cerró inesperadamente la última vez que se lo usaste. ¿Quieres compartir un informe de error con nosotros?"</string>
|
||||
<string name="rageshake_detection_dialog_content">"Parece que sacudes el teléfono con frustración. ¿Quieres abrir la pantalla de informe de errores?"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="crash_detection_dialog_content">"%1$s si è chiuso inaspettatamente l\'ultima volta che è stato usato. Vuoi condividere con noi un rapporto sull\'arresto anomalo?"</string>
|
||||
<string name="rageshake_detection_dialog_content">"Sembra che tu stia scuotendo il telefono per la frustrazione. Vuoi aprire la schermata di segnalazione dei problemi?"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="crash_detection_dialog_content">"%1$s s-a blocat ultima dată când a fost folosit. Doriți să ne trimiteți un raport?"</string>
|
||||
<string name="rageshake_detection_dialog_content">"Se pare că scuturați telefonul de frustrare. Doriți să deschdeți ecranul de raportare a unei erori?"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bug_report_attach_screenshot">"Adjuntar captura de pantalla"</string>
|
||||
<string name="screen_bug_report_contact_me">"Podéis poneros en contacto conmigo para resolver dudas relacionadas"</string>
|
||||
<string name="screen_bug_report_edit_screenshot">"Editar captura de pantalla"</string>
|
||||
<string name="screen_bug_report_editor_description">"Describe el problema. ¿Qué hiciste? ¿Qué esperabas que ocurriera? ¿Qué ocurrió en realidad? Por favor, detállalo todo lo que puedas."</string>
|
||||
<string name="screen_bug_report_editor_placeholder">"Describe el error…"</string>
|
||||
<string name="screen_bug_report_editor_supporting">"Si es posible, escriba la descripción en inglés."</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Enviar registros de fallos"</string>
|
||||
<string name="screen_bug_report_include_logs">"Enviar registros para ayudar"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Enviar captura de pantalla"</string>
|
||||
<string name="screen_bug_report_logs_description">"Para comprobar que todo funciona correctamente, se enviarán registros de fallos con su mensaje. Serán privados. Para enviar sólo tu mensaje, desactiva esta opción."</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s se cerró inesperadamente la última vez que se lo usaste. ¿Quieres compartir un informe de error con nosotros?"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bug_report_attach_screenshot">"Allega istantanea schermo"</string>
|
||||
<string name="screen_bug_report_contact_me">"Potete contattarmi per qualsiasi altra domanda"</string>
|
||||
<string name="screen_bug_report_edit_screenshot">"Modifica istantanea schermo"</string>
|
||||
<string name="screen_bug_report_editor_description">"Descrivi il bug. Che cosa hai fatto? Cosa ti aspettavi che accadesse? Cosa è effettivamente accaduto. Si prega di inserire il maggior numero di dettagli possibile."</string>
|
||||
<string name="screen_bug_report_editor_placeholder">"Descrivi il problema…"</string>
|
||||
<string name="screen_bug_report_editor_supporting">"Se possibile, scrivere la descrizione in inglese."</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Invia i log degli arresti anomali"</string>
|
||||
<string name="screen_bug_report_include_logs">"Invia i log per aiutarci"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Invia istantanea schermo"</string>
|
||||
<string name="screen_bug_report_logs_description">"Per verificare che le cose funzionino come previsto, i log verranno inviati con il tuo messaggio. Questi saranno privati. Per inviare solo il tuo messaggio, disattiva questa impostazione."</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s si è chiuso inaspettatamente l\'ultima volta che è stato usato. Vuoi condividere con noi un rapporto sull\'arresto anomalo?"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bug_report_attach_screenshot">"Atașați o captură de ecran"</string>
|
||||
<string name="screen_bug_report_contact_me">"Puteți să mă contactați dacă aveți întrebări suplimentare"</string>
|
||||
<string name="screen_bug_report_edit_screenshot">"Editați captura de ecran"</string>
|
||||
<string name="screen_bug_report_editor_description">"Vă rugăm să descrieți eroarea. Ce ați făcut? Ce vă aşteptați să se întâmple? Ce s-a întâmplat de fapt. Vă rugam să intrați în cât mai multe detalii cu putință."</string>
|
||||
<string name="screen_bug_report_editor_placeholder">"Descrieți eroarea…"</string>
|
||||
<string name="screen_bug_report_editor_supporting">"Dacă posibil, vă rugăm să scrieți descrierea în engleză."</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Trimiteți log-uri"</string>
|
||||
<string name="screen_bug_report_include_logs">"Trimiteți log-uri pentru a ajuta"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Trimiteți captură de ecran"</string>
|
||||
<string name="screen_bug_report_logs_description">"Pentru a verifica că lucrurile funcționează conform așteptărilor, log-uri vor fi trimise împreună cu mesajul. Acestea vor fi private. Pentru a trimite doar mesajul, dezactivați această setare."</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s s-a blocat ultima dată când a fost folosit. Dorești să ne trimiti un raport?"</string>
|
||||
</resources>
|
||||
|
|
@ -18,9 +18,9 @@ package io.element.android.features.roomdetails.api
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
|
||||
interface RoomDetailsEntryPoint : FeatureEntryPoint {
|
||||
fun createNode(parentNode: Node, buildContext: BuildContext): Node
|
||||
|
||||
fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List<Plugin>): Node
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ dependencies {
|
|||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.features.userlist.api)
|
||||
implementation(projects.libraries.androidutils)
|
||||
api(projects.features.roomdetails.api)
|
||||
implementation(libs.coil.compose)
|
||||
|
|
@ -52,6 +53,8 @@ dependencies {
|
|||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.userlist.impl)
|
||||
testImplementation(projects.features.userlist.test)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.roomdetails.impl
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -26,7 +27,7 @@ import javax.inject.Inject
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<RoomDetailsFlowNode>(buildContext)
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List<Plugin>): Node {
|
||||
return parentNode.createNode<RoomDetailsFlowNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,4 +16,8 @@
|
|||
|
||||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
sealed interface RoomDetailsEvent
|
||||
sealed interface RoomDetailsEvent {
|
||||
data class LeaveRoom(val needsConfirmation: Boolean) : RoomDetailsEvent
|
||||
object ClearLeaveRoomWarning : RoomDetailsEvent
|
||||
object ClearError : RoomDetailsEvent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -49,11 +51,25 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object RoomDetails : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object RoomMemberList : NavTarget
|
||||
}
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun openRoomMemberList()
|
||||
}
|
||||
|
||||
val callback = object : Callback {
|
||||
override fun openRoomMemberList() {
|
||||
backstack.push(NavTarget.RoomMemberList)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.RoomDetails -> createNode<RoomDetailsNode>(buildContext)
|
||||
NavTarget.RoomDetails -> createNode<RoomDetailsNode>(buildContext, listOf(callback))
|
||||
NavTarget.RoomMemberList -> createNode<RoomMemberListNode>(buildContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
|
|
@ -40,6 +41,12 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
private val room: MatrixRoom,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
private val callback = plugins<RoomDetailsFlowNode.Callback>().firstOrNull()
|
||||
|
||||
private fun openRoomMemberList() {
|
||||
callback?.openRoomMemberList()
|
||||
}
|
||||
|
||||
private fun onShareRoom(context: Context) {
|
||||
val alias = room.alias ?: room.alternativeAliases.firstOrNull()
|
||||
val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) }
|
||||
|
|
@ -64,6 +71,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
modifier = modifier,
|
||||
goBack = { navigateUp() },
|
||||
onShareRoom = { onShareRoom(context) },
|
||||
openRoomMemberList = ::openRoomMemberList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,17 +17,67 @@
|
|||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomDetailsPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val roomMembershipObserver: RoomMembershipObserver,
|
||||
) : Presenter<RoomDetailsState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomDetailsState {
|
||||
// fun handleEvents(event: RoomDetailsEvent) {}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var leaveRoomWarning by remember {
|
||||
mutableStateOf<LeaveRoomWarning?>(null)
|
||||
}
|
||||
var error by 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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning = null
|
||||
RoomDetailsEvent.ClearError -> error = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return RoomDetailsState(
|
||||
roomId = room.roomId.value,
|
||||
|
|
@ -35,9 +85,11 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
roomAlias = room.alias,
|
||||
roomAvatarUrl = room.avatarUrl,
|
||||
roomTopic = room.topic,
|
||||
memberCount = room.members.size,
|
||||
memberCount = memberCount,
|
||||
isEncrypted = room.isEncrypted,
|
||||
// eventSink = ::handleEvents
|
||||
displayLeaveRoomWarning = leaveRoomWarning,
|
||||
error = error,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,13 +16,40 @@
|
|||
|
||||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.isLoading
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
|
||||
data class RoomDetailsState(
|
||||
val roomId: String,
|
||||
val roomName: String,
|
||||
val roomAlias: String?,
|
||||
val roomAvatarUrl: String?,
|
||||
val roomTopic: String?,
|
||||
val memberCount: Int,
|
||||
val memberCount: Async<Int>,
|
||||
val isEncrypted: Boolean,
|
||||
// val eventSink: (RoomDetailsEvent) -> Unit
|
||||
val displayLeaveRoomWarning: LeaveRoomWarning?,
|
||||
val error: RoomDetailsError?,
|
||||
val eventSink: (RoomDetailsEvent) -> Unit
|
||||
)
|
||||
|
||||
sealed class LeaveRoomWarning {
|
||||
object Generic : LeaveRoomWarning()
|
||||
object PrivateRoom : LeaveRoomWarning()
|
||||
object LastUserInRoom : LeaveRoomWarning()
|
||||
|
||||
companion object {
|
||||
fun computeLeaveRoomWarning(isPublic: Boolean, memberCount: Async<Int>): LeaveRoomWarning {
|
||||
return when {
|
||||
!isPublic -> PrivateRoom
|
||||
(memberCount as? Async.Success<Int>)?.state == 1 -> LastUserInRoom
|
||||
else -> Generic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface RoomDetailsError {
|
||||
object AlertGeneric : RoomDetailsError
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
||||
open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState> {
|
||||
override val values: Sequence<RoomDetailsState>
|
||||
|
|
@ -25,6 +26,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
|
|||
aRoomDetailsState().copy(roomTopic = null),
|
||||
aRoomDetailsState().copy(isEncrypted = false),
|
||||
aRoomDetailsState().copy(roomAlias = null),
|
||||
aRoomDetailsState().copy(memberCount = Async.Failure(Throwable())),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
|
@ -39,7 +41,9 @@ fun aRoomDetailsState() = RoomDetailsState(
|
|||
"|| MAIL iki/Marketing " +
|
||||
"|| MAI iki/Marketing " +
|
||||
"|| MAI iki/Marketing...",
|
||||
memberCount = 32,
|
||||
memberCount = Async.Success(32),
|
||||
isEncrypted = true,
|
||||
// eventSink = {}
|
||||
displayLeaveRoomWarning = null,
|
||||
error = null,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,11 +42,15 @@ 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.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.isLoading
|
||||
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.dialogs.ErrorDialog
|
||||
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
|
||||
|
|
@ -55,6 +59,7 @@ import io.element.android.libraries.designsystem.theme.LocalColors
|
|||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -62,6 +67,7 @@ fun RoomDetailsView(
|
|||
state: RoomDetailsState,
|
||||
goBack: () -> Unit,
|
||||
onShareRoom: () -> Unit,
|
||||
openRoomMemberList: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
|
|
@ -87,13 +93,35 @@ fun RoomDetailsView(
|
|||
TopicSection(roomTopic = state.roomTopic)
|
||||
}
|
||||
|
||||
MembersSection(memberCount = state.memberCount)
|
||||
val memberCount = (state.memberCount as? Async.Success<Int>)?.state
|
||||
MembersSection(
|
||||
memberCount = memberCount,
|
||||
isLoading = state.memberCount.isLoading(),
|
||||
openRoomMemberList = openRoomMemberList
|
||||
)
|
||||
|
||||
if (state.isEncrypted) {
|
||||
SecuritySection()
|
||||
}
|
||||
|
||||
OtherActionsSection()
|
||||
OtherActionsSection(onLeaveRoom = {
|
||||
state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
|
||||
})
|
||||
|
||||
if (state.displayLeaveRoomWarning != null) {
|
||||
ConfirmLeaveRoomDialog(
|
||||
leaveRoomWarning = state.displayLeaveRoomWarning,
|
||||
onConfirmLeave = { state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) },
|
||||
onDismiss = { state.eventSink(RoomDetailsEvent.ClearLeaveRoomWarning) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.error != null) {
|
||||
ErrorDialog(
|
||||
content = stringResource(StringR.string.error_unknown),
|
||||
onDismiss = { state.eventSink(RoomDetailsEvent.ClearError) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -148,12 +176,19 @@ internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
internal fun MembersSection(memberCount: Int, modifier: Modifier = Modifier) {
|
||||
internal fun MembersSection(
|
||||
memberCount: Int?,
|
||||
isLoading: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
openRoomMemberList: () -> Unit
|
||||
) {
|
||||
PreferenceCategory(modifier = modifier) {
|
||||
PreferenceText(
|
||||
title = stringResource(R.string.screen_room_details_people_title),
|
||||
icon = Icons.Outlined.Person,
|
||||
currentValue = memberCount.toString(),
|
||||
currentValue = memberCount?.toString(),
|
||||
onClick = openRoomMemberList,
|
||||
loadingCurrentValue = isLoading,
|
||||
)
|
||||
PreferenceText(
|
||||
title = stringResource(R.string.screen_room_details_invite_people_title),
|
||||
|
|
@ -174,16 +209,38 @@ internal fun SecuritySection(modifier: Modifier = Modifier) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
internal fun OtherActionsSection(modifier: Modifier = Modifier) {
|
||||
internal fun OtherActionsSection(onLeaveRoom: () -> Unit, modifier: Modifier = Modifier) {
|
||||
PreferenceCategory(showDivider = false, modifier = modifier) {
|
||||
PreferenceText(
|
||||
title = stringResource(R.string.screen_room_details_leave_room_title),
|
||||
icon = ImageVector.vectorResource(R.drawable.ic_door_open),
|
||||
tintColor = LocalColors.current.textActionCritical,
|
||||
onClick = onLeaveRoom,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ConfirmLeaveRoomDialog(
|
||||
leaveRoomWarning: LeaveRoomWarning,
|
||||
onConfirmLeave: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val content = stringResource(
|
||||
when (leaveRoomWarning) {
|
||||
LeaveRoomWarning.PrivateRoom -> StringR.string.leave_room_alert_private_subtitle
|
||||
LeaveRoomWarning.LastUserInRoom -> StringR.string.leave_room_alert_empty_subtitle
|
||||
LeaveRoomWarning.Generic -> StringR.string.leave_room_alert_subtitle
|
||||
}
|
||||
)
|
||||
ConfirmationDialog(
|
||||
content = content,
|
||||
submitText = stringResource(StringR.string.action_leave),
|
||||
onSubmitClicked = onConfirmLeave,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RoomDetailsLightPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) =
|
||||
|
|
@ -200,5 +257,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
|
|||
state = state,
|
||||
goBack = {},
|
||||
onShareRoom = {},
|
||||
openRoomMemberList = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import javax.inject.Named
|
||||
|
||||
@Module
|
||||
@ContributesTo(RoomScope::class)
|
||||
interface RoomMemberModule {
|
||||
|
||||
@Binds
|
||||
@Named("RoomMembers")
|
||||
fun bindRoomMemberUserListDataSource(dataSource: RoomMatrixUserDataSource): MatrixUserDataSource
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdetails.impl.members
|
||||
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
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.RoomMember
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomMatrixUserDataSource @Inject constructor(
|
||||
private val room: MatrixRoom
|
||||
) : MatrixUserDataSource {
|
||||
|
||||
override suspend fun search(query: String): List<MatrixUser> {
|
||||
return room.members().filter { member ->
|
||||
if (query.isBlank()) {
|
||||
true
|
||||
} else {
|
||||
member.userId.contains(query, ignoreCase = true) || member.displayName?.contains(query, ignoreCase = true).orFalse()
|
||||
}
|
||||
}.map(::mapMemberToMatrixUser)
|
||||
}
|
||||
|
||||
override suspend fun getProfile(userId: UserId): MatrixUser? {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun mapMemberToMatrixUser(member: RoomMember): MatrixUser {
|
||||
return MatrixUser(
|
||||
id = UserId(member.userId),
|
||||
username = member.displayName,
|
||||
avatarData = AvatarData(
|
||||
id = member.userId,
|
||||
name = member.displayName,
|
||||
url = member.avatarUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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.impl.members
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
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.ui.model.MatrixUser
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class RoomMemberListNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RoomMemberListPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
private fun onUserSelected(matrixUser: MatrixUser) {
|
||||
Timber.d("TODO: implement user selection. User: $matrixUser")
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RoomMemberListView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onBackPressed = { navigateUp() },
|
||||
onUserSelected = ::onUserSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.impl.members
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
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.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
|
||||
|
||||
class RoomMemberListPresenter @Inject constructor(
|
||||
private val userListPresenterFactory: UserListPresenter.Factory,
|
||||
@Named("RoomMembers") private val matrixUserDataSource: MatrixUserDataSource,
|
||||
) : Presenter<RoomMemberListState> {
|
||||
|
||||
private val userListPresenter by lazy {
|
||||
userListPresenterFactory.create(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
matrixUserDataSource,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomMemberListState {
|
||||
val userListState = userListPresenter.present()
|
||||
val allUsers = remember { mutableStateOf<Async<ImmutableList<MatrixUser>>>(Async.Loading()) }
|
||||
LaunchedEffect(Unit) {
|
||||
withContext(Dispatchers.IO) {
|
||||
allUsers.value = Async.Success(matrixUserDataSource.search("").toImmutableList())
|
||||
}
|
||||
}
|
||||
return RoomMemberListState(
|
||||
allUsers = allUsers.value,
|
||||
userListState = userListState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.impl.members
|
||||
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
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,
|
||||
)
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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.impl.members
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
||||
override val values: Sequence<RoomMemberListState>
|
||||
get() = sequenceOf(
|
||||
aRoomMemberListState(allUsers = Async.Success(persistentListOf(aMatrixUser()))),
|
||||
aRoomMemberListState(allUsers = Async.Loading())
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aRoomMemberListState(
|
||||
searchResults: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
allUsers: Async<ImmutableList<MatrixUser>> = Async.Uninitialized,
|
||||
) =
|
||||
RoomMemberListState(
|
||||
userListState = aUserListState().copy(searchResults = searchResults),
|
||||
allUsers = allUsers,
|
||||
)
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* 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.impl.members
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.features.userlist.api.SearchSingleUserResultItem
|
||||
import io.element.android.features.userlist.api.UserListView
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.isLoading
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
||||
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.ui.model.MatrixUser
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomMemberListView(
|
||||
state: RoomMemberListState,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
onUserSelected: (MatrixUser) -> Unit = {},
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (!state.userListState.isSearchActive) {
|
||||
RoomMemberListTopBar(onBackPressed = onBackPressed)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
UserListView(
|
||||
state = state.userListState,
|
||||
onUserSelected = onUserSelected,
|
||||
)
|
||||
|
||||
if (!state.userListState.isSearchActive) {
|
||||
if (state.allUsers is Async.Success) {
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
||||
item {
|
||||
val memberCount = state.allUsers.state.count()
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
text = pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount),
|
||||
style = ElementTextStyles.Regular.callout,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
items(state.allUsers.state) { matrixUser ->
|
||||
SearchSingleUserResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
matrixUser = matrixUser,
|
||||
onClick = { onUserSelected(matrixUser) }
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (state.allUsers.isLoading()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomMemberListTopBar(
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_room_details_people_title),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
},
|
||||
navigationIcon = { BackButton(onClick = onBackPressed) },
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RoomMemberListLightPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RoomMemberListDarkPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: RoomMemberListState) {
|
||||
RoomMemberListView(state)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?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">"Una persona"</item>
|
||||
<item quantity="other">"%1$d personas"</item>
|
||||
</plurals>
|
||||
<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_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>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?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 persona"</item>
|
||||
<item quantity="other">"%1$d persone"</item>
|
||||
</plurals>
|
||||
<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_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>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?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">"o persoană"</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other">"%1$d persoane"</item>
|
||||
</plurals>
|
||||
<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>
|
||||
|
|
@ -1,5 +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 people"</item>
|
||||
</plurals>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -20,20 +20,37 @@ 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.impl.LeaveRoomWarning
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class RoomDetailsPresenterTests {
|
||||
|
||||
private val roomMembershipObserver = RoomMembershipObserver(A_SESSION_ID)
|
||||
|
||||
@Test
|
||||
fun `present - initial state is created from room info`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val presenter = RoomDetailsPresenter(room)
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -42,20 +59,149 @@ 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(room.members.count())
|
||||
Truth.assertThat(initialState.memberCount).isEqualTo(Async.Loading(null))
|
||||
Truth.assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted)
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - room member count is calculated asynchronously`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.memberCount).isEqualTo(Async.Loading(null))
|
||||
|
||||
val finalState = awaitItem()
|
||||
Truth.assertThat(finalState.memberCount).isEqualTo(Async.Success(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state with no room name`() = runTest {
|
||||
val room = aMatrixRoom(name = null)
|
||||
val presenter = RoomDetailsPresenter(room)
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.roomName).isEqualTo(room.displayName)
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - can handle error while fetching member count`() = runTest {
|
||||
val room = aMatrixRoom(name = null).apply {
|
||||
givenFetchMemberResult(Result.failure(Throwable()))
|
||||
}
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
Truth.assertThat(awaitItem().memberCount).isInstanceOf(Async.Failure::class.java)
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Leave with confirmation on private room shows a specific warning`() = runTest {
|
||||
val room = aMatrixRoom(isPublic = false)
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest {
|
||||
val room = aMatrixRoom(members = listOf(aRoomMember()))
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Leave with confirmation shows a generic warning`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Leave without confirmation leaves the room`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
// Allow room member count to load
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false))
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
|
||||
// Membership observer should receive a 'left room' change
|
||||
roomMembershipObserver.updates.take(1)
|
||||
.onEach { update -> Truth.assertThat(update.change).isEqualTo(MembershipChange.LEFT) }
|
||||
.collect()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ClearError removes any error present`() = runTest {
|
||||
val room = aMatrixRoom().apply {
|
||||
givenLeaveRoomError(Throwable())
|
||||
}
|
||||
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
|
||||
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()
|
||||
errorState.eventSink(RoomDetailsEvent.ClearError)
|
||||
Truth.assertThat(awaitItem().error).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -68,6 +214,7 @@ fun aMatrixRoom(
|
|||
avatarUrl: String? = "https://matrix.org/avatar.jpg",
|
||||
members: List<RoomMember> = emptyList(),
|
||||
isEncrypted: Boolean = true,
|
||||
isPublic: Boolean = true,
|
||||
) = FakeMatrixRoom(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
|
|
@ -76,4 +223,23 @@ fun aMatrixRoom(
|
|||
avatarUrl = avatarUrl,
|
||||
members = members,
|
||||
isEncrypted = isEncrypted,
|
||||
isPublic = isPublic,
|
||||
)
|
||||
|
||||
fun aRoomMember(
|
||||
userId: UserId = A_USER_ID,
|
||||
displayName: String? = null,
|
||||
avatarUrl: String? = null,
|
||||
membership: RoomMembershipState = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous: Boolean = false,
|
||||
powerLevel: Long = 0L,
|
||||
normalizedPowerLevel: Long = 0L
|
||||
) = RoomMember(
|
||||
userId = userId.value,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
membership = membership,
|
||||
isNameAmbiguous = isNameAmbiguous,
|
||||
powerLevel = powerLevel,
|
||||
normalizedPowerLevel = normalizedPowerLevel,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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.members
|
||||
|
||||
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.impl.members.RoomMemberListPresenter
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.features.userlist.impl.DefaultUserListPresenter
|
||||
import io.element.android.features.userlist.test.FakeMatrixUserDataSource
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.internal.toImmutableList
|
||||
import org.junit.Test
|
||||
|
||||
class RoomMemberListPresenterTests {
|
||||
|
||||
@Test
|
||||
fun `present - search is done automatically on start, but is async`() = runTest {
|
||||
val searchResult = listOf(aMatrixUser())
|
||||
val userListDataSource = FakeMatrixUserDataSource().apply {
|
||||
givenSearchResult(searchResult)
|
||||
}
|
||||
val userListFactory = object : UserListPresenter.Factory {
|
||||
override fun create(args: UserListPresenterArgs, dataSource: MatrixUserDataSource) = DefaultUserListPresenter(args, dataSource)
|
||||
}
|
||||
val presenter = RoomMemberListPresenter(userListFactory, userListDataSource)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.allUsers).isInstanceOf(Async.Loading::class.java)
|
||||
Truth.assertThat(initialState.userListState.isSearchActive).isFalse()
|
||||
Truth.assertThat(initialState.userListState.searchResults).isEmpty()
|
||||
Truth.assertThat(initialState.userListState.selectionMode).isEqualTo(SelectionMode.Single)
|
||||
|
||||
val loadedState = awaitItem()
|
||||
Truth.assertThat((loadedState.allUsers as? Async.Success)?.state).isEqualTo(searchResult.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ dependencies {
|
|||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
|
|
@ -61,6 +62,7 @@ dependencies {
|
|||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.libraries.permissions.noop)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,5 +20,4 @@ sealed interface RoomListEvents {
|
|||
data class UpdateFilter(val newFilter: String) : RoomListEvents
|
||||
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
|
||||
object DismissRequestVerificationPrompt : RoomListEvents
|
||||
object ClearSuccessfulVerificationMessage : RoomListEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,12 +34,14 @@ import io.element.android.libraries.core.extensions.orEmpty
|
|||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
|
||||
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.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomSummary
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -56,8 +58,11 @@ class RoomListPresenter @Inject constructor(
|
|||
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
|
||||
private val roomLastMessageFormatter: RoomLastMessageFormatter,
|
||||
private val sessionVerificationService: SessionVerificationService,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
) : Presenter<RoomListState> {
|
||||
|
||||
private val roomMembershipObserver: RoomMembershipObserver = client.roomMembershipObserver()
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomListState {
|
||||
val matrixUser: MutableState<MatrixUser?> = remember {
|
||||
|
|
@ -86,19 +91,11 @@ class RoomListPresenter @Inject constructor(
|
|||
derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified && !verificationPromptDismissed }
|
||||
}
|
||||
|
||||
// Current verification flow status, if any (initial, requesting, accepted, etc.)
|
||||
val currentVerificationFlowStatus by sessionVerificationService.verificationFlowState.collectAsState()
|
||||
// We only care about the 'Finished' state to display the 'verification success' message
|
||||
val presentVerificationSuccessfulMessage = remember {
|
||||
derivedStateOf { currentVerificationFlowStatus == VerificationFlowState.Finished }
|
||||
}
|
||||
|
||||
fun handleEvents(event: RoomListEvents) {
|
||||
when (event) {
|
||||
is RoomListEvents.UpdateFilter -> filter = event.newFilter
|
||||
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
|
||||
RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true
|
||||
RoomListEvents.ClearSuccessfulVerificationMessage -> sessionVerificationService.reset()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,12 +103,14 @@ class RoomListPresenter @Inject constructor(
|
|||
filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter)
|
||||
}
|
||||
|
||||
val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
|
||||
|
||||
return RoomListState(
|
||||
matrixUser = matrixUser.value,
|
||||
roomList = filteredRoomSummaries.value,
|
||||
filter = filter,
|
||||
presentVerificationSuccessfulMessage = presentVerificationSuccessfulMessage.value,
|
||||
displayVerificationPrompt = displayVerificationPrompt,
|
||||
snackbarMessage = snackbarMessage,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
|
|
|||
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