Merge branch 'develop' into feature/valere/message_shields
This commit is contained in:
commit
5dceba8146
342 changed files with 5475 additions and 1377 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -36,7 +36,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug Gplay APK
|
- name: Assemble debug Gplay APK
|
||||||
|
|
|
||||||
2
.github/workflows/build_enterprise.yml
vendored
2
.github/workflows/build_enterprise.yml
vendored
|
|
@ -44,7 +44,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug Gplay Enterprise APK
|
- name: Assemble debug Gplay Enterprise APK
|
||||||
|
|
|
||||||
2
.github/workflows/generate_github_pages.yml
vendored
2
.github/workflows/generate_github_pages.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
|
|
|
||||||
15
.github/workflows/gradle-wrapper-validation.yml
vendored
15
.github/workflows/gradle-wrapper-validation.yml
vendored
|
|
@ -1,15 +0,0 @@
|
||||||
name: "Validate Gradle Wrapper"
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
merge_group:
|
|
||||||
push:
|
|
||||||
branches: [ main, develop ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
validation:
|
|
||||||
name: "Validation"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# No concurrency required, this is a prerequisite to other actions and should run every time.
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: gradle/wrapper-validation-action@v3
|
|
||||||
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
|
|
@ -39,7 +39,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug APK
|
- name: Assemble debug APK
|
||||||
|
|
|
||||||
4
.github/workflows/nightlyReports.yml
vendored
4
.github/workflows/nightlyReports.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
|
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: false
|
cache-read-only: false
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Dependency analysis
|
- name: Dependency analysis
|
||||||
|
|
|
||||||
12
.github/workflows/quality.yml
vendored
12
.github/workflows/quality.yml
vendored
|
|
@ -52,7 +52,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
|
|
@ -90,7 +90,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Konsist tests
|
- name: Run Konsist tests
|
||||||
|
|
@ -130,7 +130,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Build Gplay Debug
|
- name: Build Gplay Debug
|
||||||
|
|
@ -174,7 +174,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Detekt
|
- name: Run Detekt
|
||||||
|
|
@ -214,7 +214,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Ktlint check
|
- name: Run Ktlint check
|
||||||
|
|
@ -254,7 +254,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Knit
|
- name: Run Knit
|
||||||
|
|
|
||||||
2
.github/workflows/recordScreenshots.yml
vendored
2
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -39,7 +39,7 @@ jobs:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
# Add gradle cache, this should speed up the process
|
# Add gradle cache, this should speed up the process
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Record screenshots
|
- name: Record screenshots
|
||||||
|
|
|
||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
- name: Create app bundle
|
- name: Create app bundle
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
|
@ -61,7 +61,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
- name: Create Enterprise app bundle
|
- name: Create Enterprise app bundle
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
|
@ -89,7 +89,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
- name: Create APKs
|
- name: Create APKs
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
|
|
||||||
2
.github/workflows/sonar.yml
vendored
2
.github/workflows/sonar.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Build Gplay Debug
|
- name: Build Gplay Debug
|
||||||
|
|
|
||||||
2
.github/workflows/sync-localazy.yml
vendored
2
.github/workflows/sync-localazy.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
|
|
|
||||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
|
@ -52,7 +52,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
|
||||||
|
|
|
||||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.24" />
|
<option name="version" value="1.9.25" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[](https://github.com/element-hq/element-x-android/actions/workflows/build.yml?query=branch%3Adevelop)
|
[](https://github.com/element-hq/element-x-android/actions/workflows/build.yml?query=branch%3Adevelop)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android)
|
[](https://sonarcloud.io/summary/new_code?id=element-x-android)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android)
|
[](https://sonarcloud.io/summary/new_code?id=element-x-android)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android)
|
[](https://sonarcloud.io/summary/new_code?id=element-x-android)
|
||||||
[](https://codecov.io/github/vector-im/element-x-android)
|
[](https://codecov.io/github/vector-im/element-x-android)
|
||||||
[](https://matrix.to/#/#element-x-android:matrix.org)
|
[](https://matrix.to/#/#element-x-android:matrix.org)
|
||||||
[](https://localazy.com/p/element)
|
[](https://localazy.com/p/element)
|
||||||
|
|
|
||||||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
|
@ -40,3 +40,5 @@
|
||||||
-keepclassmembers class android.view.JavaViewSpy {
|
-keepclassmembers class android.view.JavaViewSpy {
|
||||||
static int windowAttachCount(android.view.View);
|
static int windowAttachCount(android.view.View);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-keep class io.element.android.x.di.** { *; }
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
<locale android:name="sk"/>
|
<locale android:name="sk"/>
|
||||||
<locale android:name="sv"/>
|
<locale android:name="sv"/>
|
||||||
<locale android:name="uk"/>
|
<locale android:name="uk"/>
|
||||||
|
<locale android:name="uz"/>
|
||||||
<locale android:name="zh-CN"/>
|
<locale android:name="zh-CN"/>
|
||||||
<locale android:name="zh-TW"/>
|
<locale android:name="zh-TW"/>
|
||||||
</locale-config>
|
</locale-config>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ object TimelineConfig {
|
||||||
StateEventType.ROOM_GUEST_ACCESS,
|
StateEventType.ROOM_GUEST_ACCESS,
|
||||||
StateEventType.ROOM_HISTORY_VISIBILITY,
|
StateEventType.ROOM_HISTORY_VISIBILITY,
|
||||||
StateEventType.ROOM_JOIN_RULES,
|
StateEventType.ROOM_JOIN_RULES,
|
||||||
StateEventType.ROOM_PINNED_EVENTS,
|
|
||||||
StateEventType.ROOM_POWER_LEVELS,
|
StateEventType.ROOM_POWER_LEVELS,
|
||||||
StateEventType.ROOM_SERVER_ACL,
|
StateEventType.ROOM_SERVER_ACL,
|
||||||
StateEventType.ROOM_TOMBSTONE,
|
StateEventType.ROOM_TOMBSTONE,
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ allprojects {
|
||||||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.5")
|
detektPlugins("io.nlopez.compose.rules:detekt:0.4.10")
|
||||||
}
|
}
|
||||||
|
|
||||||
// KtLint
|
// KtLint
|
||||||
|
|
@ -129,11 +129,11 @@ dependencyAnalysis {
|
||||||
// To run a sonar analysis:
|
// To run a sonar analysis:
|
||||||
// Run './gradlew sonar -Dsonar.login=<SONAR_LOGIN>'
|
// Run './gradlew sonar -Dsonar.login=<SONAR_LOGIN>'
|
||||||
// The SONAR_LOGIN is stored in passbolt as Token Sonar Cloud Bma
|
// The SONAR_LOGIN is stored in passbolt as Token Sonar Cloud Bma
|
||||||
// Sonar result can be found here: https://sonarcloud.io/project/overview?id=vector-im_element-x-android
|
// Sonar result can be found here: https://sonarcloud.io/project/overview?id=element-x-android
|
||||||
sonar {
|
sonar {
|
||||||
properties {
|
properties {
|
||||||
property("sonar.projectName", "element-x-android")
|
property("sonar.projectName", "element-x-android")
|
||||||
property("sonar.projectKey", "vector-im_element-x-android")
|
property("sonar.projectKey", "element-x-android")
|
||||||
property("sonar.host.url", "https://sonarcloud.io")
|
property("sonar.host.url", "https://sonarcloud.io")
|
||||||
property("sonar.projectVersion", "1.0") // TODO project(":app").android.defaultConfig.versionName)
|
property("sonar.projectVersion", "1.0") // TODO project(":app").android.defaultConfig.versionName)
|
||||||
property("sonar.sourceEncoding", "UTF-8")
|
property("sonar.sourceEncoding", "UTF-8")
|
||||||
|
|
@ -141,7 +141,7 @@ sonar {
|
||||||
property("sonar.links.ci", "https://github.com/element-hq/element-x-android/actions")
|
property("sonar.links.ci", "https://github.com/element-hq/element-x-android/actions")
|
||||||
property("sonar.links.scm", "https://github.com/element-hq/element-x-android/")
|
property("sonar.links.scm", "https://github.com/element-hq/element-x-android/")
|
||||||
property("sonar.links.issue", "https://github.com/element-hq/element-x-android/issues")
|
property("sonar.links.issue", "https://github.com/element-hq/element-x-android/issues")
|
||||||
property("sonar.organization", "new_vector_ltd_organization")
|
property("sonar.organization", "element-hq")
|
||||||
property("sonar.login", if (project.hasProperty("SONAR_LOGIN")) project.property("SONAR_LOGIN")!! else "invalid")
|
property("sonar.login", if (project.hasProperty("SONAR_LOGIN")) project.property("SONAR_LOGIN")!! else "invalid")
|
||||||
|
|
||||||
// exclude source code from analyses separated by a colon (:)
|
// exclude source code from analyses separated by a colon (:)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane dotyczące użytkowania, aby pomóc nam identyfikować problemy."</string>
|
<string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane dotyczące użytkowania, aby pomóc nam identyfikować problemy."</string>
|
||||||
<string name="screen_analytics_settings_read_terms">"Możesz przeczytać wszystkie nasze warunki %1$s."</string>
|
<string name="screen_analytics_settings_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
|
||||||
<string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string>
|
<string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string>
|
||||||
<string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string>
|
<string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"Muammolarni aniqlashda yordam berish uchun anonim foydalanish maʼlumotlarini baham koʻring."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"Siz bizning barcha shartlarimizni o\'qishingiz mumkin%1$s."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"Bu yerga"</string>
|
||||||
|
<string name="screen_analytics_settings_share_data">"Analitik ma\'lumotlarni ulashish"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_prompt_data_usage">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string>
|
<string name="screen_analytics_prompt_data_usage">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string>
|
||||||
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane dotyczące użytkowania, aby pomóc nam identyfikować problemy."</string>
|
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane dotyczące użytkowania, aby pomóc nam identyfikować problemy."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms">"Możesz przeczytać wszystkie nasze warunki %1$s."</string>
|
<string name="screen_analytics_prompt_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string>
|
<string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string>
|
||||||
<string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string>
|
<string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string>
|
||||||
<string name="screen_analytics_prompt_third_party_sharing">"Nie będziemy udostępniać Twoich danych podmiotom trzecim"</string>
|
<string name="screen_analytics_prompt_third_party_sharing">"Nie będziemy udostępniać Twoich danych podmiotom trzecim"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_prompt_data_usage">"Biz hech qanday shaxsiy ma\'lumotlarni yozmaymiz yoki profilga kiritmaymiz"</string>
|
||||||
|
<string name="screen_analytics_prompt_help_us_improve">"Muammolarni aniqlashda yordam berish uchun anonim foydalanish maʼlumotlarini baham koʻring."</string>
|
||||||
|
<string name="screen_analytics_prompt_read_terms">"Siz bizning barcha shartlarimizni o\'qishingiz mumkin%1$s."</string>
|
||||||
|
<string name="screen_analytics_prompt_read_terms_content_link">"Bu yerga"</string>
|
||||||
|
<string name="screen_analytics_prompt_settings">"Buni istalgan vaqtda oʻchirib qoʻyishingiz mumkin"</string>
|
||||||
|
<string name="screen_analytics_prompt_third_party_sharing">"Biz sizning ma\'lumotlaringizni uchinchi tomonlar bilan baham ko\'rmaymiz"</string>
|
||||||
|
<string name="screen_analytics_prompt_title">"Yaxshilashga yordam bering%1$s"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -3,4 +3,5 @@
|
||||||
<string name="call_foreground_service_channel_title_android">"Połączenie w trakcie"</string>
|
<string name="call_foreground_service_channel_title_android">"Połączenie w trakcie"</string>
|
||||||
<string name="call_foreground_service_message_android">"Stuknij, aby wrócić do rozmowy"</string>
|
<string name="call_foreground_service_message_android">"Stuknij, aby wrócić do rozmowy"</string>
|
||||||
<string name="call_foreground_service_title_android">"☎️ Rozmowa w toku"</string>
|
<string name="call_foreground_service_title_android">"☎️ Rozmowa w toku"</string>
|
||||||
|
<string name="screen_incoming_call_subtitle_android">"Przychodzące połączenie Element"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@
|
||||||
<string name="call_foreground_service_channel_title_android">"Pågående samtal"</string>
|
<string name="call_foreground_service_channel_title_android">"Pågående samtal"</string>
|
||||||
<string name="call_foreground_service_message_android">"Tryck för att återgå till samtalet"</string>
|
<string name="call_foreground_service_message_android">"Tryck för att återgå till samtalet"</string>
|
||||||
<string name="call_foreground_service_title_android">"☎️ Samtal pågår"</string>
|
<string name="call_foreground_service_title_android">"☎️ Samtal pågår"</string>
|
||||||
|
<string name="screen_incoming_call_subtitle_android">"Inkommande Element Call"</string>
|
||||||
</resources>
|
</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="call_foreground_service_channel_title_android">"Davom etayotgan qo\'ng\'iroq"</string>
|
||||||
|
<string name="call_foreground_service_message_android">"Qo\'ng\'iroqqa qaytish uchun bosing"</string>
|
||||||
|
<string name="call_foreground_service_title_android">"☎️ Qo‘ng‘iroq davom etmoqda"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||||
import io.element.android.services.analytics.api.ScreenTracker
|
import io.element.android.services.analytics.api.ScreenTracker
|
||||||
|
|
@ -43,11 +44,13 @@ import io.element.android.services.analytics.test.FakeScreenTracker
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilTimeout
|
import io.element.android.tests.testutils.consumeItemsUntilTimeout
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -86,8 +89,9 @@ class CallScreenPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - with CallType RoomCall sets call as active, loads URL, runs WidgetDriver and notifies the other clients a call started`() = runTest {
|
fun `present - with CallType RoomCall sets call as active, loads URL, runs WidgetDriver and notifies the other clients a call started`() = runTest {
|
||||||
val sendCallNotificationIfNeededLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
val sendCallNotificationIfNeededLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val syncService = FakeSyncService(MutableStateFlow(SyncState.Running))
|
||||||
val fakeRoom = FakeMatrixRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda)
|
val fakeRoom = FakeMatrixRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda)
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient(syncService = syncService).apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, fakeRoom)
|
givenGetRoomResult(A_ROOM_ID, fakeRoom)
|
||||||
}
|
}
|
||||||
val widgetDriver = FakeMatrixWidgetDriver()
|
val widgetDriver = FakeMatrixWidgetDriver()
|
||||||
|
|
@ -216,7 +220,12 @@ class CallScreenPresenterTest {
|
||||||
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
|
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
|
||||||
val navigator = FakeCallScreenNavigator()
|
val navigator = FakeCallScreenNavigator()
|
||||||
val widgetDriver = FakeMatrixWidgetDriver()
|
val widgetDriver = FakeMatrixWidgetDriver()
|
||||||
val matrixClient = FakeMatrixClient()
|
val syncStateFlow = MutableStateFlow(SyncState.Idle)
|
||||||
|
val startSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val syncService = FakeSyncService(syncStateFlow = syncStateFlow).apply {
|
||||||
|
this.startSyncLambda = startSyncLambda
|
||||||
|
}
|
||||||
|
val matrixClient = FakeMatrixClient(syncService = syncService)
|
||||||
val presenter = createCallScreenPresenter(
|
val presenter = createCallScreenPresenter(
|
||||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||||
widgetDriver = widgetDriver,
|
widgetDriver = widgetDriver,
|
||||||
|
|
@ -230,7 +239,7 @@ class CallScreenPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
consumeItemsUntilTimeout()
|
consumeItemsUntilTimeout()
|
||||||
|
|
||||||
assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Running)
|
assert(startSyncLambda).isCalledOnce()
|
||||||
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +249,12 @@ class CallScreenPresenterTest {
|
||||||
fun `present - automatically stops the Matrix client sync on dispose`() = runTest {
|
fun `present - automatically stops the Matrix client sync on dispose`() = runTest {
|
||||||
val navigator = FakeCallScreenNavigator()
|
val navigator = FakeCallScreenNavigator()
|
||||||
val widgetDriver = FakeMatrixWidgetDriver()
|
val widgetDriver = FakeMatrixWidgetDriver()
|
||||||
val matrixClient = FakeMatrixClient()
|
val syncStateFlow = MutableStateFlow(SyncState.Running)
|
||||||
|
val stopSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val syncService = FakeSyncService(syncStateFlow = syncStateFlow).apply {
|
||||||
|
this.stopSyncLambda = stopSyncLambda
|
||||||
|
}
|
||||||
|
val matrixClient = FakeMatrixClient(syncService = syncService)
|
||||||
val presenter = createCallScreenPresenter(
|
val presenter = createCallScreenPresenter(
|
||||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||||
widgetDriver = widgetDriver,
|
widgetDriver = widgetDriver,
|
||||||
|
|
@ -262,7 +276,7 @@ class CallScreenPresenterTest {
|
||||||
|
|
||||||
job.cancelAndJoin()
|
job.cancelAndJoin()
|
||||||
|
|
||||||
assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Terminated)
|
assert(stopSyncLambda).isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createCallScreenPresenter(
|
private fun TestScope.createCallScreenPresenter(
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<string name="screen_create_room_private_option_description">"Wiadomości w tym pokoju są szyfrowane. Szyfrowania nie można później wyłączyć."</string>
|
<string name="screen_create_room_private_option_description">"Wiadomości w tym pokoju są szyfrowane. Szyfrowania nie można później wyłączyć."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Pokój prywatny (tylko zaproszenie)"</string>
|
<string name="screen_create_room_private_option_title">"Pokój prywatny (tylko zaproszenie)"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Wiadomości nie są szyfrowane i każdy może je odczytać. Możesz aktywować szyfrowanie później."</string>
|
<string name="screen_create_room_public_option_description">"Wiadomości nie są szyfrowane i każdy może je odczytać. Możesz aktywować szyfrowanie później."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Pokój publiczny (każdy)"</string>
|
<string name="screen_create_room_public_option_title">"Pokój publiczny (wszyscy)"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Nazwa pokoju"</string>
|
<string name="screen_create_room_room_name_label">"Nazwa pokoju"</string>
|
||||||
<string name="screen_create_room_title">"Utwórz pokój"</string>
|
<string name="screen_create_room_title">"Utwórz pokój"</string>
|
||||||
<string name="screen_create_room_topic_label">"Temat (opcjonalnie)"</string>
|
<string name="screen_create_room_topic_label">"Temat (opcjonalnie)"</string>
|
||||||
|
|
|
||||||
|
|
@ -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_create_room_action_create_room">"Yangi xona"</string>
|
||||||
|
<string name="screen_create_room_add_people_title">"Odamlarni taklif qiling"</string>
|
||||||
|
<string name="screen_create_room_error_creating_room">"Xonani yaratishda xatolik yuz berdi"</string>
|
||||||
|
<string name="screen_create_room_private_option_description">"Bu xonadagi xabarlar shifrlangan. Keyinchalik shifrlashni o‘chirib bo‘lmaydi."</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"Shaxsiy xona (faqat taklif)"</string>
|
||||||
|
<string name="screen_create_room_public_option_description">"Xabarlar shifrlanmagan va har kim ularni o\'qiy oladi. Keyinchalik shifrlashni yoqishingiz mumkin."</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"Jamoat xonasi (har kim)"</string>
|
||||||
|
<string name="screen_create_room_room_name_label">"Xona nomi"</string>
|
||||||
|
<string name="screen_create_room_title">"Xonani yaratish"</string>
|
||||||
|
<string name="screen_create_room_topic_label">"Mavzu (ixtiyoriy)"</string>
|
||||||
|
<string name="screen_start_chat_error_starting_chat">"Suhbatni boshlashda xatolik yuz berdi"</string>
|
||||||
|
</resources>
|
||||||
11
features/ftue/impl/src/main/res/values-uz/translations.xml
Normal file
11
features/ftue/impl/src/main/res/values-uz/translations.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_notification_optin_subtitle">"Sozlamalaringizni keyinroq o\'zgartirishingiz mumkin."</string>
|
||||||
|
<string name="screen_notification_optin_title">"Bildirishnomalarga ruxsat bering va hech qachon xabarni o\'tkazib yubormang"</string>
|
||||||
|
<string name="screen_welcome_bullet_1">"Qo\'ng\'iroqlar, so\'ro\'vlar, qidiruv va boshqalar shu yil oxirida qo\'shiladi."</string>
|
||||||
|
<string name="screen_welcome_bullet_2">"Shifrlangan xonalar uchun xabarlar tarixi hali mavjud emas."</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"Biz sizdan eshitishni istardik, sozlamalar sahifasi orqali fikringizni bildiring."</string>
|
||||||
|
<string name="screen_welcome_button">"Qani ketdik!"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"Buni bilishingiz kerak:"</string>
|
||||||
|
<string name="screen_welcome_title">"%1$sga Xush kelibsiz!"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_invites_decline_chat_message">"Czy na pewno chcesz odrzucić zaproszenie do dołączenia do %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string>
|
<string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_title">"Odrzuć czat"</string>
|
<string name="screen_invites_decline_direct_chat_title">"Odrzuć czat"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_invites_decline_chat_message">"Haqiqatan ham qo\'shilish taklifini rad qilmoqchimisiz%1$s ?"</string>
|
||||||
|
<string name="screen_invites_decline_chat_title">"Taklifni rad etish"</string>
|
||||||
|
<string name="screen_invites_decline_direct_chat_message">"Haqiqatan ham bu shaxsiy chatni rad qilmoqchimisiz%1$s ?"</string>
|
||||||
|
<string name="screen_invites_decline_direct_chat_title">"Chatni rad etish"</string>
|
||||||
|
<string name="screen_invites_empty_list">"Takliflar yo\'q"</string>
|
||||||
|
<string name="screen_invites_invited_you">"%1$s(%2$s ) sizni taklif qildi"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_join_room_join_action">"Dołącz do pokoju"</string>
|
||||||
|
<string name="screen_join_room_knock_action">"Zapukaj, by dołączyć"</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_description">"%1$s jeszcze nie obsługuje przestrzeni. Uzyskaj dostęp do przestrzeni w wersji web."</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"Przestrzenie nie są jeszcze obsługiwane"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Kliknij przycisk poniżej, aby powiadomić administratora pokoju. Po zatwierdzeniu będziesz mógł dołączyć do rozmowy."</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"Musisz być członkiem tego pokoju, aby wyświetlić historię wiadomości."</string>
|
||||||
|
<string name="screen_join_room_title_knock">"Chcesz dołączyć do tego pokoju?"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"Podgląd nie jest dostępny"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_join_room_join_action">"Gå med i rummet"</string>
|
||||||
|
<string name="screen_join_room_knock_action">"Knacka för att gå med"</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_description">"%1$s stöder inte utrymmen än. Du kan komma åt utrymmen på webben."</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"Utrymmen stöds inte ännu"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Klicka på knappen nedan så kommer en rumsadministratör att meddelas. Du kommer att kunna gå med i konversationen när den har godkänts."</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"Du måste vara medlem i det här rummet för att se meddelandehistoriken."</string>
|
||||||
|
<string name="screen_join_room_title_knock">"Vill du gå med i det här rummet?"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"Förhandsgranskning är inte tillgänglig"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_join_room_join_action">"Приєднатися до кімнати"</string>
|
||||||
|
<string name="screen_join_room_knock_action">"Постукати, щоб приєднатися"</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_description">"%1$s ще не підтримує простори. Ви можете отримати доступ до них в вебверсії."</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"Простори поки що не підтримуються"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Натисніть кнопку нижче, і адміністратор кімнати отримає сповіщення. Ви зможете приєднатися до розмови після схвалення."</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"Ви мусите бути учасником цієї кімнати, щоб переглядати історію повідомлень."</string>
|
||||||
|
<string name="screen_join_room_title_knock">"Хочете приєднатися до цієї кімнати?"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"Попередній перегляд недоступний"</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="leave_room_alert_empty_subtitle">"Bu xonani tark etmoqchi ekanligingizga ishonchingiz komilmi? Siz bu yerda yagona odamsiz. Agar siz tark etsangiz, kelajakda hech kim qo\'shila olmaydi, jumladan siz ham."</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"Bu xonani tark etmoqchi ekanligingizga ishonchingiz komilmi? Bu xona ochiq emas va siz taklifsiz qayta qo‘shila olmaysiz."</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"Xonani tark etmoqchi ekanligingizga ishonchingiz komilmi?"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -41,6 +41,7 @@ dependencies {
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
implementation(projects.libraries.cryptography.api)
|
implementation(projects.libraries.cryptography.api)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
|
implementation(projects.features.logout.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.sessionStorage.api)
|
implementation(projects.libraries.sessionStorage.api)
|
||||||
implementation(projects.services.appnavstate.api)
|
implementation(projects.services.appnavstate.api)
|
||||||
|
|
@ -59,4 +60,5 @@ dependencies {
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.libraries.sessionStorage.test)
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
|
testImplementation(projects.features.logout.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockMana
|
||||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||||
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
|
import io.element.android.features.logout.api.LogoutUseCase
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
|
@ -41,7 +41,7 @@ import javax.inject.Inject
|
||||||
class PinUnlockPresenter @Inject constructor(
|
class PinUnlockPresenter @Inject constructor(
|
||||||
private val pinCodeManager: PinCodeManager,
|
private val pinCodeManager: PinCodeManager,
|
||||||
private val biometricUnlockManager: BiometricUnlockManager,
|
private val biometricUnlockManager: BiometricUnlockManager,
|
||||||
private val signOut: SignOut,
|
private val logoutUseCase: LogoutUseCase,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val pinUnlockHelper: PinUnlockHelper,
|
private val pinUnlockHelper: PinUnlockHelper,
|
||||||
) : Presenter<PinUnlockState> {
|
) : Presenter<PinUnlockState> {
|
||||||
|
|
@ -179,7 +179,7 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
|
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
signOut()
|
logoutUseCase.logout(ignoreSdkError = true)
|
||||||
}.runCatchingUpdatingState(signOutAction)
|
}.runCatchingUpdatingState(signOutAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@ package io.element.android.features.lockscreen.impl.unlock.di
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
|
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(SessionScope::class)
|
||||||
interface PinUnlockBindings {
|
interface PinUnlockBindings {
|
||||||
fun inject(activity: PinUnlockActivity)
|
fun inject(activity: PinUnlockActivity)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 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.lockscreen.impl.unlock.signout
|
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
|
||||||
import io.element.android.libraries.di.AppScope
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
class DefaultSignOut @Inject constructor(
|
|
||||||
private val authenticationService: MatrixAuthenticationService,
|
|
||||||
private val matrixClientProvider: MatrixClientProvider,
|
|
||||||
) : SignOut {
|
|
||||||
override suspend fun invoke(): String? {
|
|
||||||
val currentSession = authenticationService.getLatestSessionId()
|
|
||||||
return if (currentSession != null) {
|
|
||||||
matrixClientProvider.getOrRestore(currentSession)
|
|
||||||
.getOrThrow()
|
|
||||||
.logout(ignoreSdkError = true)
|
|
||||||
} else {
|
|
||||||
error("No session to sign out")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -16,11 +16,11 @@
|
||||||
<string name="screen_app_lock_setup_confirm_pin">"Potwierdź PIN"</string>
|
<string name="screen_app_lock_setup_confirm_pin">"Potwierdź PIN"</string>
|
||||||
<string name="screen_app_lock_setup_pin_context">"Zablokuj %1$s, aby zwiększyć bezpieczeństwo swoich czatów.
|
<string name="screen_app_lock_setup_pin_context">"Zablokuj %1$s, aby zwiększyć bezpieczeństwo swoich czatów.
|
||||||
|
|
||||||
Wybierz coś łatwego do zapamiętania. Jeśli zapomnisz tego PINU, zostaniesz wylogowany z aplikacji."</string>
|
Wybierz coś łatwego do zapamiętania. Jeśli zapomnisz tego PIN\'u, zostaniesz wylogowany z aplikacji."</string>
|
||||||
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Nie możesz wybrać tego PINU ze względów bezpieczeństwa"</string>
|
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Nie możesz wybrać tego PIN\'u ze względów bezpieczeństwa"</string>
|
||||||
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Wybierz inny kod PIN"</string>
|
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Wybierz inny kod PIN"</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Wprowadź ten sam kod PIN dwa razy"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Wprowadź ten sam kod PIN dwa razy"</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINY nie pasują do siebie"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN\'y nie pasują do siebie"</string>
|
||||||
<string name="screen_app_lock_signout_alert_message">"Aby kontynuować, zaloguj się ponownie i utwórz nowy kod PIN"</string>
|
<string name="screen_app_lock_signout_alert_message">"Aby kontynuować, zaloguj się ponownie i utwórz nowy kod PIN"</string>
|
||||||
<string name="screen_app_lock_signout_alert_title">"Trwa wylogowywanie"</string>
|
<string name="screen_app_lock_signout_alert_title">"Trwa wylogowywanie"</string>
|
||||||
<plurals name="screen_app_lock_subtitle">
|
<plurals name="screen_app_lock_subtitle">
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,27 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_app_lock_biometric_authentication">"autenticação por biometria"</string>
|
||||||
|
<string name="screen_app_lock_biometric_unlock">"desbloqueio por biometria"</string>
|
||||||
|
<string name="screen_app_lock_biometric_unlock_title_android">"Desbloquear com biometria"</string>
|
||||||
<string name="screen_app_lock_forgot_pin">"Esqueceu o PIN?"</string>
|
<string name="screen_app_lock_forgot_pin">"Esqueceu o PIN?"</string>
|
||||||
<string name="screen_app_lock_settings_change_pin">"Mudar código de PIN"</string>
|
<string name="screen_app_lock_settings_change_pin">"Alterar código de PIN"</string>
|
||||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Permitir desbloqueio biométrico"</string>
|
<string name="screen_app_lock_settings_enable_biometric_unlock">"Permitir desbloqueio biométrico"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin">"Remover PIN"</string>
|
<string name="screen_app_lock_settings_remove_pin">"Remover PIN"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin_alert_message">"Tem certeza de que quer remover o PIN?"</string>
|
<string name="screen_app_lock_settings_remove_pin_alert_message">"Tem certeza de que quer remover o PIN?"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin_alert_title">"Remover PIN?"</string>
|
<string name="screen_app_lock_settings_remove_pin_alert_title">"Remover PIN?"</string>
|
||||||
|
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Permitir %1$s"</string>
|
||||||
|
<string name="screen_app_lock_setup_biometric_unlock_skip">"Prefiro usar o PIN"</string>
|
||||||
|
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Poupe tempo e use %1$s para desbloquear o aplicativo todas as vezes"</string>
|
||||||
<string name="screen_app_lock_setup_choose_pin">"Escolher PIN"</string>
|
<string name="screen_app_lock_setup_choose_pin">"Escolher PIN"</string>
|
||||||
<string name="screen_app_lock_setup_confirm_pin">"Confirmar PIN"</string>
|
<string name="screen_app_lock_setup_confirm_pin">"Confirmar PIN"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_context">"Bloqueie o %1$s para adicionar uma segurança extra às suas conversas.
|
||||||
|
|
||||||
|
Escolha algo memorável. Se você esquecer este PIN, você será desconectado do app."</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Você não pode escolher este PIN por razões de segurança"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Escolha um PIN diferente"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Por favor, insira o mesmo PIN duas vezes"</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Os PINs não correspondem"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Os PINs não correspondem"</string>
|
||||||
|
<string name="screen_app_lock_signout_alert_message">"Você terá que fazer login novamente e criar um novo PIN para prosseguir"</string>
|
||||||
<string name="screen_app_lock_signout_alert_title">"Você está sendo desconectado"</string>
|
<string name="screen_app_lock_signout_alert_title">"Você está sendo desconectado"</string>
|
||||||
<plurals name="screen_app_lock_subtitle">
|
<plurals name="screen_app_lock_subtitle">
|
||||||
<item quantity="one">"Você tem %1$d tentativa de debloqueio"</item>
|
<item quantity="one">"Você tem %1$d tentativa de debloqueio"</item>
|
||||||
|
|
@ -18,5 +31,7 @@
|
||||||
<item quantity="one">"PIN incorreto. Você tem mais %1$d chance"</item>
|
<item quantity="one">"PIN incorreto. Você tem mais %1$d chance"</item>
|
||||||
<item quantity="other">"PIN incorreto. Você tem mais %1$d chances"</item>
|
<item quantity="other">"PIN incorreto. Você tem mais %1$d chances"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="screen_app_lock_use_biometric_android">"Usar biometria"</string>
|
||||||
|
<string name="screen_app_lock_use_pin_android">"Usar PIN"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Saindo…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Saindo…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_signout_in_progress_dialog_content">"Chiqish…"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 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.lockscreen.impl.unlock
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.features.lockscreen.impl.unlock.signout.DefaultSignOut
|
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class DefaultSignOutTest {
|
|
||||||
private val matrixClient = FakeMatrixClient()
|
|
||||||
private val authenticationService = FakeMatrixAuthenticationService()
|
|
||||||
private val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) })
|
|
||||||
private val sut = DefaultSignOut(authenticationService, matrixClientProvider)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when no active session then it throws`() = runTest {
|
|
||||||
authenticationService.getLatestSessionIdLambda = { null }
|
|
||||||
val result = runCatching { sut.invoke() }
|
|
||||||
assertThat(result.isFailure).isTrue()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `with one active session and successful logout on client`() = runTest {
|
|
||||||
val logoutLambda = lambdaRecorder<Boolean, String?> { _: Boolean -> null }
|
|
||||||
authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId }
|
|
||||||
matrixClient.logoutLambda = logoutLambda
|
|
||||||
val result = runCatching { sut.invoke() }
|
|
||||||
assertThat(result.isSuccess).isTrue()
|
|
||||||
assert(logoutLambda).isCalledOnce()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `with one active session and and failed logout on client`() = runTest {
|
|
||||||
val logoutLambda = lambdaRecorder<Boolean, String?> { _: Boolean -> error("Failed to logout") }
|
|
||||||
authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId }
|
|
||||||
matrixClient.logoutLambda = logoutLambda
|
|
||||||
val result = runCatching { sut.invoke() }
|
|
||||||
assertThat(result.isFailure).isTrue()
|
|
||||||
assert(logoutLambda).isCalledOnce()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -28,7 +28,7 @@ import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.assertText
|
import io.element.android.features.lockscreen.impl.pin.model.assertText
|
||||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||||
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
|
import io.element.android.features.logout.test.FakeLogoutUseCase
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -106,9 +106,9 @@ class PinUnlockPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - forgot pin flow`() = runTest {
|
fun `present - forgot pin flow`() = runTest {
|
||||||
val signOutLambda = lambdaRecorder<String?> { null }
|
val signOutLambda = lambdaRecorder<Boolean, String> { "" }
|
||||||
val signOut = FakeSignOut(signOutLambda)
|
val signOut = FakeLogoutUseCase(signOutLambda)
|
||||||
val presenter = createPinUnlockPresenter(this, signOut = signOut)
|
val presenter = createPinUnlockPresenter(this, logoutUseCase = signOut)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -135,7 +135,7 @@ class PinUnlockPresenterTest {
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java)
|
||||||
}
|
}
|
||||||
assert(signOutLambda).isCalledOnce().withNoParameter()
|
assert(signOutLambda).isCalledOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +147,7 @@ class PinUnlockPresenterTest {
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(),
|
biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(),
|
||||||
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
||||||
signOut: SignOut = FakeSignOut(),
|
logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }),
|
||||||
): PinUnlockPresenter {
|
): PinUnlockPresenter {
|
||||||
val pinCodeManager = aPinCodeManager().apply {
|
val pinCodeManager = aPinCodeManager().apply {
|
||||||
addCallback(callback)
|
addCallback(callback)
|
||||||
|
|
@ -156,7 +156,7 @@ class PinUnlockPresenterTest {
|
||||||
return PinUnlockPresenter(
|
return PinUnlockPresenter(
|
||||||
pinCodeManager = pinCodeManager,
|
pinCodeManager = pinCodeManager,
|
||||||
biometricUnlockManager = biometricUnlockManager,
|
biometricUnlockManager = biometricUnlockManager,
|
||||||
signOut = signOut,
|
logoutUseCase = logoutUseCase,
|
||||||
coroutineScope = scope,
|
coroutineScope = scope,
|
||||||
pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager),
|
pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<string name="screen_account_provider_form_subtitle">"Szukaj serwera firmowego, społeczności lub prywatnego."</string>
|
<string name="screen_account_provider_form_subtitle">"Szukaj serwera firmowego, społeczności lub prywatnego."</string>
|
||||||
<string name="screen_account_provider_form_title">"Znajdź dostawcę konta"</string>
|
<string name="screen_account_provider_form_title">"Znajdź dostawcę konta"</string>
|
||||||
<string name="screen_account_provider_signin_subtitle">"Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."</string>
|
<string name="screen_account_provider_signin_subtitle">"Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."</string>
|
||||||
<string name="screen_account_provider_signin_title">"Zamierzasz się zalogować %s"</string>
|
<string name="screen_account_provider_signin_title">"Zamierzasz zalogować się do %s"</string>
|
||||||
<string name="screen_account_provider_signup_subtitle">"Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."</string>
|
<string name="screen_account_provider_signup_subtitle">"Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."</string>
|
||||||
<string name="screen_account_provider_signup_title">"Zamierzasz założyć konto na %s"</string>
|
<string name="screen_account_provider_signup_title">"Zamierzasz założyć konto na %s"</string>
|
||||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org jest ogromnym i darmowym serwerem na publicznej sieci Matrix zapewniający bezpieczną i zdecentralizowaną komunikację zarządzaną przez Fundację Matrix.org."</string>
|
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org jest ogromnym i darmowym serwerem na publicznej sieci Matrix zapewniający bezpieczną i zdecentralizowaną komunikację zarządzaną przez Fundację Matrix.org."</string>
|
||||||
|
|
@ -14,20 +14,64 @@
|
||||||
<string name="screen_change_account_provider_subtitle">"Użyj innego dostawcy konta, takiego jak własny serwer lub konta służbowego."</string>
|
<string name="screen_change_account_provider_subtitle">"Użyj innego dostawcy konta, takiego jak własny serwer lub konta służbowego."</string>
|
||||||
<string name="screen_change_account_provider_title">"Zmień dostawcę konta"</string>
|
<string name="screen_change_account_provider_title">"Zmień dostawcę konta"</string>
|
||||||
<string name="screen_change_server_error_invalid_homeserver">"Nie mogliśmy połączyć się z tym serwerem domowym. Sprawdź, czy adres URL serwera został wprowadzony poprawnie. Jeśli adres URL jest poprawny, skontaktuj się z administratorem serwera w celu uzyskania dalszej pomocy."</string>
|
<string name="screen_change_server_error_invalid_homeserver">"Nie mogliśmy połączyć się z tym serwerem domowym. Sprawdź, czy adres URL serwera został wprowadzony poprawnie. Jeśli adres URL jest poprawny, skontaktuj się z administratorem serwera w celu uzyskania dalszej pomocy."</string>
|
||||||
|
<string name="screen_change_server_error_invalid_well_known">"Sliding sync nie jest dostępny z powodu problemu w znanym pliku:
|
||||||
|
%1$s"</string>
|
||||||
<string name="screen_change_server_error_no_sliding_sync_message">"Ten serwer obecnie nie obsługuje technologii Sliding Sync."</string>
|
<string name="screen_change_server_error_no_sliding_sync_message">"Ten serwer obecnie nie obsługuje technologii Sliding Sync."</string>
|
||||||
<string name="screen_change_server_form_header">"Adres URL serwera domowego"</string>
|
<string name="screen_change_server_form_header">"URL serwera domowego"</string>
|
||||||
<string name="screen_change_server_form_notice">"Możesz połączyć się tylko z serwerem, który obsługuje technologię Sliding Sync. Administrator serwera domowego będzie musiał ją skonfigurować. %1$s"</string>
|
<string name="screen_change_server_form_notice">"Możesz połączyć się tylko z serwerem, który obsługuje technologię Sliding Sync. Administrator serwera domowego będzie musiał ją skonfigurować. %1$s"</string>
|
||||||
<string name="screen_change_server_subtitle">"Jaki jest adres Twojego serwera?"</string>
|
<string name="screen_change_server_subtitle">"Jaki jest adres Twojego serwera?"</string>
|
||||||
<string name="screen_change_server_title">"Wybierz swój serwer"</string>
|
<string name="screen_change_server_title">"Wybierz swój serwer"</string>
|
||||||
<string name="screen_login_error_deactivated_account">"To konto zostało dezaktywowane."</string>
|
<string name="screen_login_error_deactivated_account">"To konto zostało dezaktywowane."</string>
|
||||||
<string name="screen_login_error_invalid_credentials">"Nieprawidłowa nazwa użytkownika i/lub hasło"</string>
|
<string name="screen_login_error_invalid_credentials">"Nieprawidłowa nazwa użytkownika i/lub hasło"</string>
|
||||||
<string name="screen_login_error_invalid_user_id">"To nie jest prawidłowy identyfikator użytkownika. Oczekiwany format: \'@user:homeserver.org\'"</string>
|
<string name="screen_login_error_invalid_user_id">"To nie jest prawidłowy identyfikator użytkownika. Oczekiwany format: \'@user:homeserver.org\'"</string>
|
||||||
|
<string name="screen_login_error_refresh_tokens">"Ten serwer został skonfigurowany do korzystania z tokenów odświeżania. Nie są one obsługiwane, gdy korzystasz z hasła."</string>
|
||||||
<string name="screen_login_error_unsupported_authentication">"Wybrany serwer domowy nie obsługuje uwierzytelniania hasłem, ani OIDC. Skontaktuj się z jego administratorem lub wybierz inny serwer domowy."</string>
|
<string name="screen_login_error_unsupported_authentication">"Wybrany serwer domowy nie obsługuje uwierzytelniania hasłem, ani OIDC. Skontaktuj się z jego administratorem lub wybierz inny serwer domowy."</string>
|
||||||
<string name="screen_login_form_header">"Wprowadź swoje dane"</string>
|
<string name="screen_login_form_header">"Wprowadź swoje dane"</string>
|
||||||
<string name="screen_login_subtitle">"Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."</string>
|
<string name="screen_login_subtitle">"Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."</string>
|
||||||
<string name="screen_login_title">"Witaj ponownie!"</string>
|
<string name="screen_login_title">"Witaj ponownie!"</string>
|
||||||
<string name="screen_login_title_with_homeserver">"Zaloguj się do %1$s"</string>
|
<string name="screen_login_title_with_homeserver">"Zaloguj się do %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"Nawiązanie bezpiecznego połączenia"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nie udało się nawiązać bezpiecznego połączenia z nowym urządzeniem. Twoje istniejące urządzenia są nadal bezpieczne i nie musisz się o nie martwić."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Co teraz?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Spróbuj zalogować się ponownie za pomocą kodu QR, jeśli byłby to problem z siecią"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Jeśli napotkasz ten sam problem, użyj innej sieci Wi-FI lub danych mobilnych"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Jeśli to nie zadziała, zaloguj się ręcznie"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Połączenie nie jest bezpieczne"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Zostaniesz poproszony o wprowadzenie dwóch cyfr widocznych na tym urządzeniu."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Wprowadź numer poniżej na innym urządzeniu"</string>
|
||||||
|
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"Zaloguj się na drugie urządzenie lub użyj tego, które jest już zalogowane, a następnie spróbuj ponownie."</string>
|
||||||
|
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Drugie urządzenie nie jest zalogowane"</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_subtitle">"Logowanie zostało anulowane na drugim urządzeniu."</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_title">"Prośba o logowanie została anulowana"</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_subtitle">"Logowanie zostało odrzucone na drugim urządzeniu."</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_title">"Logowanie odrzucone"</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_subtitle">"Logowanie wygasło. Spróbuj ponownie."</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_title">"Logowanie nie zostało ukończone na czas"</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Twoje drugie urządzenie nie wspiera logowania się do %s za pomocą kodu QR.
|
||||||
|
|
||||||
|
Spróbuj zalogować się ręcznie lub zeskanuj kod QR na innym urządzeniu."</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_title">"Kod QR nie jest wspierany"</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Twój dostawca konta nie obsługuje %1$s."</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s nie jest wspierany"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Gotowy do skanowania"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_1">"Otwórz %1$s na urządzeniu stacjonarnym"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_2">"Kliknij na swój awatar"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3">"Wybierz %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Powiąż nowe urządzenie”"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4">"Zeskanuj kod QR za pomocą tego urządzenia"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_title">"Otwórz %1$s na innym urządzeniu, aby uzyskać kod QR"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Użyj kodu QR widocznego na drugim urządzeniu."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Spróbuj ponownie"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Spróbuj ponownie"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Błędny kod QR"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Przejdź do ustawień aparatu"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Musisz przyznać uprawnienia %1$s do korzystania z kamery, aby kontynuować."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Zezwól na dostęp do kamery, aby zeskanować kod QR"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"Skanuj kod QR"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Zacznij od nowa"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Wystąpił nieoczekiwany błąd. Spróbuj ponownie."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Oczekiwanie na drugie urządzenie"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Twój dostawca konta może poprosić o podany kod, aby zweryfikować logowanie."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Twój kod weryfikacyjny"</string>
|
||||||
<string name="screen_server_confirmation_change_server">"Zmień dostawcę konta"</string>
|
<string name="screen_server_confirmation_change_server">"Zmień dostawcę konta"</string>
|
||||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Serwer prywatny dla pracowników Element."</string>
|
<string name="screen_server_confirmation_message_login_element_dot_io">"Serwer prywatny dla pracowników Element."</string>
|
||||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."</string>
|
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."</string>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,48 @@
|
||||||
<string name="screen_login_subtitle">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string>
|
<string name="screen_login_subtitle">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string>
|
||||||
<string name="screen_login_title">"Välkommen tillbaka!"</string>
|
<string name="screen_login_title">"Välkommen tillbaka!"</string>
|
||||||
<string name="screen_login_title_with_homeserver">"Logga in på %1$s"</string>
|
<string name="screen_login_title_with_homeserver">"Logga in på %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"Upprättar en säker anslutning"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"En säker anslutning kunde inte göras till den nya enheten. Dina befintliga enheter är fortfarande säkra och du behöver inte oroa dig för dem."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Nu då?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Pröva att logga in igen med en QR-kod ifall detta skulle vara ett nätverksproblem"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Om du stöter på samma problem, prova ett annat wifi-nätverk eller använd din mobildata istället för wifi"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Om det inte fungerar, logga in manuellt"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Anslutningen är inte säker"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Du kommer att bli ombedd att ange de två siffrorna som visas på den här enheten."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Ange numret nedan på din andra enhet"</string>
|
||||||
|
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"Logga in på din andra enhet och försök sedan igen, eller använd en annan enhet som redan är inloggad."</string>
|
||||||
|
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Den andra enheten är inte inloggad"</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_subtitle">"Inloggningen avbröts på den andra enheten."</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_title">"Inloggningsförfrågan avbröts"</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_subtitle">"Inloggningen avvisades på den andra enheten."</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_title">"Inloggning avvisad"</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_subtitle">"Inloggningen har löpt ut. Vänligen försök igen."</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_title">"Inloggningen slutfördes inte i tid"</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Din andra enhet stöder inte inloggning i %s med en QR-kod.
|
||||||
|
|
||||||
|
Prova att logga in manuellt eller skanna QR-koden med en annan enhet."</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-kod stöds inte"</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Din kontoleverantör stöder inte %1$s."</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s stöds inte"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Redo att skanna"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_1">"Öppna %1$s på en skrivbordsenhet"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_2">"Klicka på din avatar"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3">"Välj %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3_action">"”Länka ny enhet”"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4">"Skanna QR-koden med den här enheten"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_title">"Öppna %1$s på en annan enhet för att få QR-koden"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Använd QR-koden som visas på den andra enheten."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Försök igen"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Försök igen"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Fel QR-kod"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Gå till kamerainställningar"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Du måste ge tillstånd för %1$s att använda enhetens kamera för att kunna fortsätta."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Tillåt kameraåtkomst för att skanna QR-koden"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"Skanna QR-koden"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Börja om"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Ett oväntat fel inträffade. Vänligen försök igen."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Väntar på din andra enhet"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Din kontoleverantör kan be om följande kod för att verifiera inloggningen."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Din verifieringskod"</string>
|
||||||
<string name="screen_server_confirmation_change_server">"Byt kontoleverantör"</string>
|
<string name="screen_server_confirmation_change_server">"Byt kontoleverantör"</string>
|
||||||
<string name="screen_server_confirmation_message_login_element_dot_io">"En privat server för Element-anställda."</string>
|
<string name="screen_server_confirmation_message_login_element_dot_io">"En privat server för Element-anställda."</string>
|
||||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string>
|
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,48 @@
|
||||||
<string name="screen_login_subtitle">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string>
|
<string name="screen_login_subtitle">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string>
|
||||||
<string name="screen_login_title">"З поверненням!"</string>
|
<string name="screen_login_title">"З поверненням!"</string>
|
||||||
<string name="screen_login_title_with_homeserver">"Увійти в %1$s"</string>
|
<string name="screen_login_title_with_homeserver">"Увійти в %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"Встановлення безпечного з\'єднання"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не вдалося встановити безпечне з\'єднання з новим пристроєм. Ваші існуючі пристрої все ще в безпеці, і вам не потрібно про них турбуватися."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Що тепер?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Спробуйте увійти ще раз за допомогою QR-коду, якщо це була проблема з мережею"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Якщо ви зіткнулися з тією ж проблемою, спробуйте іншу мережу Wi-Fi або використовуйте мобільний інтернет замість Wi-Fi"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Якщо це не спрацює, увійдіть вручну"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"З\'єднання не є безпечним"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Вас попросять ввести дві цифри, показані на цьому пристрої."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Введіть номер нижче на іншому пристрої"</string>
|
||||||
|
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"Увійдіть на іншому пристрої та спробуйте ще раз або скористайтеся іншим пристроєм, що вже в обліковому записі."</string>
|
||||||
|
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Інший пристрій не ввійшов"</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_subtitle">"Вхід було скасовано на іншому пристрої."</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_title">"Запит на вхід скасовано"</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_subtitle">"Вхід був відхилений на іншому пристрої."</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_title">"Вхід відхилено"</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_subtitle">"Термін входу сплив. Будь ласка, спробуйте ще раз."</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_title">"Вхід не було завершено вчасно"</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Ваш інший пристрій не підтримує вхід у %s за допомогою QR-коду.
|
||||||
|
|
||||||
|
Спробуйте ввійти вручну або відскануйте QR-код за допомогою іншого пристрою."</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-код не підтримується"</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Постачальник вашого облікового запису не підтримує %1$s."</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s не підтримується"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Готовий до сканування"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_1">"Відкрийте %1$s на комп\'ютері"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_2">"Натисніть на свою аватарку"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3">"Оберіть %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Підключити новий пристрій”"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4">"Відскануйте QR-код цим пристроєм"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_title">"Відкрийте %1$s на іншому пристрої, щоб отримати QR-код"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Використовуйте QR-код, показаний на іншому пристрої."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Спробуйте ще раз"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Спробуйте ще раз"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Неправильний QR-код"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Перейти до налаштувань камери"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Вам потрібно дати дозвіл %1$s на використання камери вашого пристрою, щоб продовжити."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Надайте доступ до камери, щоб сканувати QR-код"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"Відскануйте QR-код"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Почати спочатку"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Сталася несподівана помилка. Будь ласка, спробуйте ще раз."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Чекаємо на ваш інший пристрій"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Постачальник облікового запису може попросити вас ввести код нижче для підтвердження входу."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Ваш код підтвердження"</string>
|
||||||
<string name="screen_server_confirmation_change_server">"Змінити провайдера облікового запису"</string>
|
<string name="screen_server_confirmation_change_server">"Змінити провайдера облікового запису"</string>
|
||||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Приватний сервер для співробітників Element."</string>
|
<string name="screen_server_confirmation_message_login_element_dot_io">"Приватний сервер для співробітників Element."</string>
|
||||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string>
|
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string>
|
||||||
|
|
|
||||||
42
features/login/impl/src/main/res/values-uz/translations.xml
Normal file
42
features/login/impl/src/main/res/values-uz/translations.xml
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_account_provider_change">"Hisob provayderini o\'zgartiring"</string>
|
||||||
|
<string name="screen_account_provider_form_hint">"Uy server manzili"</string>
|
||||||
|
<string name="screen_account_provider_form_notice">"Qidiruv so\'zini yoki domen manzilini kiriting."</string>
|
||||||
|
<string name="screen_account_provider_form_subtitle">"Kompaniya, jamoa yoki shaxsiy serverni qidiring."</string>
|
||||||
|
<string name="screen_account_provider_form_title">"Hisob provayderini toping"</string>
|
||||||
|
<string name="screen_account_provider_signin_subtitle">"Bu sizning suhbatlaringiz yashaydigan joy - xuddi siz elektron pochta xabarlaringizni saqlash uchun elektron pochta provayderidan foydalanganingiz kabi."</string>
|
||||||
|
<string name="screen_account_provider_signin_title">"Siz %sga kirmoqchisiz"</string>
|
||||||
|
<string name="screen_account_provider_signup_subtitle">"Bu sizning suhbatlaringiz yashaydigan joy - xuddi siz elektron pochta xabarlaringizni saqlash uchun elektron pochta provayderidan foydalanganingiz kabi."</string>
|
||||||
|
<string name="screen_account_provider_signup_title">"Siz %sda hisob yaratmoqchisiz"</string>
|
||||||
|
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org - bu Matrix.org Jamg\'armasi tomonidan boshqariladigan xavfsiz, markazlashtirilmagan aloqa uchun ommaviy Matrix tarmog\'idagi katta, bepul server."</string>
|
||||||
|
<string name="screen_change_account_provider_other">"Boshqa"</string>
|
||||||
|
<string name="screen_change_account_provider_subtitle">"Shaxsiy serveringiz yoki ishchi hisob qaydnomangiz kabi boshqa hisob provayderidan foydalaning."</string>
|
||||||
|
<string name="screen_change_account_provider_title">"Hisob provayderini o\'zgartiring"</string>
|
||||||
|
<string name="screen_change_server_error_invalid_homeserver">"Bu uy serveriga kira olmadik. Iltimos, uy serverining URL manzilini to\'ri kiritganingizni tekshiring. Agar URL toʻgʻri boʻlsa, qoʻshimcha yordam olish uchun uy serveri administratoriga murojaat qiling."</string>
|
||||||
|
<string name="screen_change_server_error_no_sliding_sync_message">"Hozirda bu server siljish sinxronlashni qo‘llab-quvvatlamaydi."</string>
|
||||||
|
<string name="screen_change_server_form_header">"Uy serverining URL manzili"</string>
|
||||||
|
<string name="screen_change_server_form_notice">"Siz faqat siljish sinxronlashni qo\'llab-quvvatlaydigan mavjud serverga ulanishingiz mumkin. Uy serveringiz administratori uni sozlashi kerak.%1$s"</string>
|
||||||
|
<string name="screen_change_server_subtitle">"Serveringizning manzili nima?"</string>
|
||||||
|
<string name="screen_change_server_title">"Serveringizni tanlang"</string>
|
||||||
|
<string name="screen_login_error_deactivated_account">"Bu hisob o‘chirilgan."</string>
|
||||||
|
<string name="screen_login_error_invalid_credentials">"Notog\'ri foydalanuvchi nomi va/yoki parol"</string>
|
||||||
|
<string name="screen_login_error_invalid_user_id">"Bu haqiqiy foydalanuvchi identifikatori emas. Kutilayotgan format: \'@user:homeserver.org\'"</string>
|
||||||
|
<string name="screen_login_error_unsupported_authentication">"Tanlangan uy serveri parol yoki OIDC loginni qo\'lab-quvvatlamaydi. Iltimos, administratoringizga murojaat qiling yoki boshqa uy serverini tanlang."</string>
|
||||||
|
<string name="screen_login_form_header">"Tafsilotlaringizni kiriting"</string>
|
||||||
|
<string name="screen_login_subtitle">"Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."</string>
|
||||||
|
<string name="screen_login_title">"Qaytib kelganingizdan xursandmiz!"</string>
|
||||||
|
<string name="screen_login_title_with_homeserver">"Kirish%1$s"</string>
|
||||||
|
<string name="screen_server_confirmation_change_server">"Hisob provayderini o\'zgartiring"</string>
|
||||||
|
<string name="screen_server_confirmation_message_login_element_dot_io">"Element xodimlari uchun shaxsiy server."</string>
|
||||||
|
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."</string>
|
||||||
|
<string name="screen_server_confirmation_message_register">"Bu sizning suhbatlaringiz yashaydigan joy - xuddi siz elektron pochta xabarlaringizni saqlash uchun elektron pochta provayderidan foydalanganingiz kabi."</string>
|
||||||
|
<string name="screen_server_confirmation_title_login">"Siz tizimga kirmoqchisiz%1$s"</string>
|
||||||
|
<string name="screen_server_confirmation_title_register">"Hisob yaratmoqchisiz%1$s"</string>
|
||||||
|
<string name="screen_waitlist_message">"Hozirgi paytda %2$sga %1$sda talab yuqori. Bir necha kundan keyin ilovaga qayting va qaytadan urining.
|
||||||
|
|
||||||
|
Sabr-toqatingiz uchun rahmat!"</string>
|
||||||
|
<string name="screen_waitlist_message_success">"%1$sga Xush kelibsiz!"</string>
|
||||||
|
<string name="screen_waitlist_title">"Siz deyarli keldingiz."</string>
|
||||||
|
<string name="screen_waitlist_title_success">"Siz kirdingiz."</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.logout.api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to trigger a log out of the current user from any part of the app.
|
||||||
|
*/
|
||||||
|
interface LogoutUseCase {
|
||||||
|
/**
|
||||||
|
* Log out the current user and then perform any needed cleanup tasks.
|
||||||
|
* @param ignoreSdkError if true, the SDK error will be ignored and the user will be logged out anyway.
|
||||||
|
* @return the session id of the logged out user.
|
||||||
|
*/
|
||||||
|
suspend fun logout(ignoreSdkError: Boolean): String
|
||||||
|
|
||||||
|
interface Factory {
|
||||||
|
fun create(sessionId: String): LogoutUseCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.logout.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.features.logout.api.LogoutUseCase
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
|
class DefaultLogoutUseCase @AssistedInject constructor(
|
||||||
|
@Assisted private val sessionId: String,
|
||||||
|
private val matrixClientProvider: MatrixClientProvider,
|
||||||
|
) : LogoutUseCase {
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory : LogoutUseCase.Factory {
|
||||||
|
override fun create(sessionId: String): DefaultLogoutUseCase
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun logout(ignoreSdkError: Boolean): String {
|
||||||
|
val matrixClient = matrixClientProvider.getOrRestore(SessionId(sessionId)).getOrThrow()
|
||||||
|
matrixClient.logout(ignoreSdkError = ignoreSdkError)
|
||||||
|
return sessionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.logout.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import io.element.android.features.logout.api.LogoutUseCase
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@ContributesTo(SessionScope::class)
|
||||||
|
object SessionLogoutModule {
|
||||||
|
@Provides
|
||||||
|
fun provideLogoutUseCase(
|
||||||
|
currentSessionIdHolder: CurrentSessionIdHolder,
|
||||||
|
factory: DefaultLogoutUseCase.Factory,
|
||||||
|
): LogoutUseCase {
|
||||||
|
return factory.create(currentSessionIdHolder.current.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,5 +4,9 @@
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Sair"</string>
|
<string name="screen_signout_confirmation_dialog_submit">"Sair"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Sair"</string>
|
<string name="screen_signout_confirmation_dialog_title">"Sair"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Saindo…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Saindo…"</string>
|
||||||
|
<string name="screen_signout_key_backup_disabled_title">"Você desativou o backup"</string>
|
||||||
|
<string name="screen_signout_key_backup_ongoing_title">"O backup das suas chaves ainda está em andamento"</string>
|
||||||
<string name="screen_signout_preference_item">"Sair"</string>
|
<string name="screen_signout_preference_item">"Sair"</string>
|
||||||
|
<string name="screen_signout_recovery_disabled_title">"A recuperação não está configurada"</string>
|
||||||
|
<string name="screen_signout_save_recovery_key_title">"Você salvou sua chave de recuperação?"</string>
|
||||||
</resources>
|
</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">"Haqiqatan ham tizimdan chiqmoqchimisiz?"</string>
|
||||||
|
<string name="screen_signout_confirmation_dialog_submit">"Tizimdan chiqish"</string>
|
||||||
|
<string name="screen_signout_confirmation_dialog_title">"Tizimdan chiqish"</string>
|
||||||
|
<string name="screen_signout_in_progress_dialog_content">"Chiqish…"</string>
|
||||||
|
<string name="screen_signout_preference_item">"Tizimdan chiqish"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
|
@ -14,15 +14,16 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.lockscreen.impl.unlock
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
|
}
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
|
||||||
|
android {
|
||||||
class FakeSignOut(
|
namespace = "io.element.android.features.logout.test"
|
||||||
var lambda: () -> String? = { null }
|
}
|
||||||
) : SignOut {
|
|
||||||
override suspend fun invoke(): String? = simulateLongTask {
|
dependencies {
|
||||||
lambda()
|
implementation(libs.coroutines.core)
|
||||||
}
|
implementation(projects.tests.testutils)
|
||||||
|
api(projects.features.logout.api)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.logout.test
|
||||||
|
|
||||||
|
import io.element.android.features.logout.api.LogoutUseCase
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
|
class FakeLogoutUseCase(
|
||||||
|
var logoutLambda: (Boolean) -> String = lambdaError()
|
||||||
|
) : LogoutUseCase {
|
||||||
|
override suspend fun logout(ignoreSdkError: Boolean): String {
|
||||||
|
return logoutLambda(ignoreSdkError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -102,5 +102,6 @@ dependencies {
|
||||||
testImplementation(projects.features.poll.test)
|
testImplementation(projects.features.poll.test)
|
||||||
testImplementation(projects.features.poll.impl)
|
testImplementation(projects.features.poll.impl)
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testImplementation(projects.libraries.eventformatter.test)
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,11 @@ import kotlin.math.roundToInt
|
||||||
* @param modifier The modifier for the layout.
|
* @param modifier The modifier for the layout.
|
||||||
* @param sheetContentKey The key for the sheet content. If the key changes, the sheet will be remeasured.
|
* @param sheetContentKey The key for the sheet content. If the key changes, the sheet will be remeasured.
|
||||||
*/
|
*/
|
||||||
@Suppress("ContentTrailingLambda")
|
@Suppress(
|
||||||
|
"ContentTrailingLambda",
|
||||||
|
// False positive
|
||||||
|
"MultipleEmitters",
|
||||||
|
)
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ExpandableBottomSheetScaffold(
|
internal fun ExpandableBottomSheetScaffold(
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
class MessagesFlowNode @AssistedInject constructor(
|
class MessagesFlowNode @AssistedInject constructor(
|
||||||
|
|
@ -217,6 +218,10 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
|
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
|
||||||
elementCallEntryPoint.startCall(callType)
|
elementCallEntryPoint.startCall(callType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onViewAllPinnedEvents() {
|
||||||
|
Timber.d("On View All Pinned Events not implemented yet.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val inputs = MessagesNode.Inputs(
|
val inputs = MessagesNode.Inputs(
|
||||||
focusedEventId = inputs.focusedEventId,
|
focusedEventId = inputs.focusedEventId,
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
fun onCreatePollClick()
|
fun onCreatePollClick()
|
||||||
fun onEditPollClick(eventId: EventId)
|
fun onEditPollClick(eventId: EventId)
|
||||||
fun onJoinCallClick(roomId: RoomId)
|
fun onJoinCallClick(roomId: RoomId)
|
||||||
|
fun onViewAllPinnedEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
|
@ -185,6 +186,10 @@ class MessagesNode @AssistedInject constructor(
|
||||||
callbacks.forEach { it.onEditPollClick(eventId) }
|
callbacks.forEach { it.onEditPollClick(eventId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onViewAllPinnedMessagesClick() {
|
||||||
|
callbacks.forEach { it.onViewAllPinnedEvents() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun onSendLocationClick() {
|
private fun onSendLocationClick() {
|
||||||
callbacks.forEach { it.onSendLocationClick() }
|
callbacks.forEach { it.onSendLocationClick() }
|
||||||
}
|
}
|
||||||
|
|
@ -221,6 +226,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
onSendLocationClick = this::onSendLocationClick,
|
onSendLocationClick = this::onSendLocationClick,
|
||||||
onCreatePollClick = this::onCreatePollClick,
|
onCreatePollClick = this::onCreatePollClick,
|
||||||
onJoinCallClick = this::onJoinCallClick,
|
onJoinCallClick = this::onJoinCallClick,
|
||||||
|
onViewAllPinnedMessagesClick = this::onViewAllPinnedMessagesClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,12 @@ import android.os.Build
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
|
@ -39,6 +41,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
|
|
@ -73,12 +76,13 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.isDm
|
import io.element.android.libraries.matrix.api.room.isDm
|
||||||
|
import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin
|
||||||
|
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
||||||
|
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
||||||
|
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.room.canCall
|
import io.element.android.libraries.matrix.ui.room.canCall
|
||||||
import io.element.android.libraries.matrix.ui.room.canRedactOtherAsState
|
|
||||||
import io.element.android.libraries.matrix.ui.room.canRedactOwnAsState
|
|
||||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
@ -98,6 +102,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
private val customReactionPresenter: CustomReactionPresenter,
|
private val customReactionPresenter: CustomReactionPresenter,
|
||||||
private val reactionSummaryPresenter: ReactionSummaryPresenter,
|
private val reactionSummaryPresenter: ReactionSummaryPresenter,
|
||||||
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
|
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
|
||||||
|
private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
|
@ -129,12 +134,12 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
val customReactionState = customReactionPresenter.present()
|
val customReactionState = customReactionPresenter.present()
|
||||||
val reactionSummaryState = reactionSummaryPresenter.present()
|
val reactionSummaryState = reactionSummaryPresenter.present()
|
||||||
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
|
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
|
||||||
|
val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present()
|
||||||
|
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
|
||||||
val userHasPermissionToRedactOwn by room.canRedactOwnAsState(updateKey = syncUpdateFlow.value)
|
val userEventPermissions by userEventPermissions(syncUpdateFlow.value)
|
||||||
val userHasPermissionToRedactOther by room.canRedactOtherAsState(updateKey = syncUpdateFlow.value)
|
|
||||||
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION, updateKey = syncUpdateFlow.value)
|
|
||||||
val roomName: AsyncData<String> by remember {
|
val roomName: AsyncData<String> by remember {
|
||||||
derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
||||||
}
|
}
|
||||||
|
|
@ -211,11 +216,8 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
roomAvatar = roomAvatar,
|
roomAvatar = roomAvatar,
|
||||||
heroes = heroes,
|
heroes = heroes,
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
|
||||||
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
|
|
||||||
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
|
|
||||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
|
||||||
composerState = composerState,
|
composerState = composerState,
|
||||||
|
userEventPermissions = userEventPermissions,
|
||||||
voiceMessageComposerState = voiceMessageComposerState,
|
voiceMessageComposerState = voiceMessageComposerState,
|
||||||
timelineState = timelineState,
|
timelineState = timelineState,
|
||||||
typingNotificationState = typingNotificationState,
|
typingNotificationState = typingNotificationState,
|
||||||
|
|
@ -231,10 +233,24 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
enableVoiceMessages = enableVoiceMessages,
|
enableVoiceMessages = enableVoiceMessages,
|
||||||
appName = buildMeta.applicationName,
|
appName = buildMeta.applicationName,
|
||||||
callState = callState,
|
callState = callState,
|
||||||
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun userEventPermissions(updateKey: Long): State<UserEventPermissions> {
|
||||||
|
return produceState(UserEventPermissions.DEFAULT, key1 = updateKey) {
|
||||||
|
value = UserEventPermissions(
|
||||||
|
canSendMessage = room.canSendMessage(type = MessageEventType.ROOM_MESSAGE).getOrElse { true },
|
||||||
|
canSendReaction = room.canSendMessage(type = MessageEventType.REACTION).getOrElse { true },
|
||||||
|
canRedactOwn = room.canRedactOwn().getOrElse { false },
|
||||||
|
canRedactOther = room.canRedactOther().getOrElse { false },
|
||||||
|
canPinUnpin = room.canPinUnpin().getOrElse { false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun MatrixRoomInfo.avatarData(): AvatarData {
|
private fun MatrixRoomInfo.avatarData(): AvatarData {
|
||||||
return AvatarData(
|
return AvatarData(
|
||||||
id = id.value,
|
id = id.value,
|
||||||
|
|
@ -268,6 +284,30 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
TimelineItemAction.Forward -> handleForwardAction(targetEvent)
|
TimelineItemAction.Forward -> handleForwardAction(targetEvent)
|
||||||
TimelineItemAction.ReportContent -> handleReportAction(targetEvent)
|
TimelineItemAction.ReportContent -> handleReportAction(targetEvent)
|
||||||
TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent, timelineState)
|
TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent, timelineState)
|
||||||
|
TimelineItemAction.Pin -> handlePinAction(targetEvent)
|
||||||
|
TimelineItemAction.Unpin -> handleUnpinAction(targetEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handlePinAction(targetEvent: TimelineItem.Event) {
|
||||||
|
if (targetEvent.eventId == null) return
|
||||||
|
timelineController.invokeOnCurrentTimeline {
|
||||||
|
pinEvent(targetEvent.eventId)
|
||||||
|
.onFailure {
|
||||||
|
Timber.e(it, "Failed to pin event ${targetEvent.eventId}")
|
||||||
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) {
|
||||||
|
if (targetEvent.eventId == null) return
|
||||||
|
timelineController.invokeOnCurrentTimeline {
|
||||||
|
unpinEvent(targetEvent.eventId)
|
||||||
|
.onFailure {
|
||||||
|
Timber.e(it, "Failed to unpin event ${targetEvent.eventId}")
|
||||||
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineState
|
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||||
|
|
@ -37,10 +38,7 @@ data class MessagesState(
|
||||||
val roomName: AsyncData<String>,
|
val roomName: AsyncData<String>,
|
||||||
val roomAvatar: AsyncData<AvatarData>,
|
val roomAvatar: AsyncData<AvatarData>,
|
||||||
val heroes: ImmutableList<AvatarData>,
|
val heroes: ImmutableList<AvatarData>,
|
||||||
val userHasPermissionToSendMessage: Boolean,
|
val userEventPermissions: UserEventPermissions,
|
||||||
val userHasPermissionToRedactOwn: Boolean,
|
|
||||||
val userHasPermissionToRedactOther: Boolean,
|
|
||||||
val userHasPermissionToSendReaction: Boolean,
|
|
||||||
val composerState: MessageComposerState,
|
val composerState: MessageComposerState,
|
||||||
val voiceMessageComposerState: VoiceMessageComposerState,
|
val voiceMessageComposerState: VoiceMessageComposerState,
|
||||||
val timelineState: TimelineState,
|
val timelineState: TimelineState,
|
||||||
|
|
@ -57,6 +55,7 @@ data class MessagesState(
|
||||||
val enableVoiceMessages: Boolean,
|
val enableVoiceMessages: Boolean,
|
||||||
val callState: RoomCallState,
|
val callState: RoomCallState,
|
||||||
val appName: String,
|
val appName: String,
|
||||||
|
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
||||||
val eventSink: (MessagesEvents) -> Unit
|
val eventSink: (MessagesEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineState
|
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||||
|
|
@ -53,7 +55,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
aMessagesState(),
|
aMessagesState(),
|
||||||
aMessagesState(hasNetworkConnection = false),
|
aMessagesState(hasNetworkConnection = false),
|
||||||
aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)),
|
aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)),
|
||||||
aMessagesState(userHasPermissionToSendMessage = false),
|
aMessagesState(userEventPermissions = aUserEventPermissions(canSendMessage = false)),
|
||||||
aMessagesState(showReinvitePrompt = true),
|
aMessagesState(showReinvitePrompt = true),
|
||||||
aMessagesState(
|
aMessagesState(
|
||||||
roomName = AsyncData.Uninitialized,
|
roomName = AsyncData.Uninitialized,
|
||||||
|
|
@ -87,16 +89,19 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
aMessagesState(
|
aMessagesState(
|
||||||
callState = RoomCallState.DISABLED,
|
callState = RoomCallState.DISABLED,
|
||||||
),
|
),
|
||||||
|
aMessagesState(
|
||||||
|
pinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(
|
||||||
|
knownPinnedMessagesCount = 4,
|
||||||
|
currentPinnedMessageIndex = 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aMessagesState(
|
fun aMessagesState(
|
||||||
roomName: AsyncData<String> = AsyncData.Success("Room name"),
|
roomName: AsyncData<String> = AsyncData.Success("Room name"),
|
||||||
roomAvatar: AsyncData<AvatarData> = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
|
roomAvatar: AsyncData<AvatarData> = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
|
||||||
userHasPermissionToSendMessage: Boolean = true,
|
userEventPermissions: UserEventPermissions = aUserEventPermissions(),
|
||||||
userHasPermissionToRedactOwn: Boolean = false,
|
|
||||||
userHasPermissionToRedactOther: Boolean = false,
|
|
||||||
userHasPermissionToSendReaction: Boolean = true,
|
|
||||||
composerState: MessageComposerState = aMessageComposerState(
|
composerState: MessageComposerState = aMessageComposerState(
|
||||||
textEditorState = TextEditorState.Rich(aRichTextEditorState(initialText = "Hello", initialFocus = true)),
|
textEditorState = TextEditorState.Rich(aRichTextEditorState(initialText = "Hello", initialFocus = true)),
|
||||||
isFullScreen = false,
|
isFullScreen = false,
|
||||||
|
|
@ -116,16 +121,14 @@ fun aMessagesState(
|
||||||
showReinvitePrompt: Boolean = false,
|
showReinvitePrompt: Boolean = false,
|
||||||
enableVoiceMessages: Boolean = true,
|
enableVoiceMessages: Boolean = true,
|
||||||
callState: RoomCallState = RoomCallState.ENABLED,
|
callState: RoomCallState = RoomCallState.ENABLED,
|
||||||
|
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
|
||||||
eventSink: (MessagesEvents) -> Unit = {},
|
eventSink: (MessagesEvents) -> Unit = {},
|
||||||
) = MessagesState(
|
) = MessagesState(
|
||||||
roomId = RoomId("!id:domain"),
|
roomId = RoomId("!id:domain"),
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
roomAvatar = roomAvatar,
|
roomAvatar = roomAvatar,
|
||||||
heroes = persistentListOf(),
|
heroes = persistentListOf(),
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
userEventPermissions = userEventPermissions,
|
||||||
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
|
|
||||||
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
|
|
||||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
|
||||||
composerState = composerState,
|
composerState = composerState,
|
||||||
voiceMessageComposerState = voiceMessageComposerState,
|
voiceMessageComposerState = voiceMessageComposerState,
|
||||||
typingNotificationState = aTypingNotificationState(),
|
typingNotificationState = aTypingNotificationState(),
|
||||||
|
|
@ -142,9 +145,24 @@ fun aMessagesState(
|
||||||
enableVoiceMessages = enableVoiceMessages,
|
enableVoiceMessages = enableVoiceMessages,
|
||||||
callState = callState,
|
callState = callState,
|
||||||
appName = "Element",
|
appName = "Element",
|
||||||
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun aUserEventPermissions(
|
||||||
|
canRedactOwn: Boolean = false,
|
||||||
|
canRedactOther: Boolean = false,
|
||||||
|
canSendMessage: Boolean = true,
|
||||||
|
canSendReaction: Boolean = true,
|
||||||
|
canPinUnpin: Boolean = false,
|
||||||
|
) = UserEventPermissions(
|
||||||
|
canRedactOwn = canRedactOwn,
|
||||||
|
canRedactOther = canRedactOther,
|
||||||
|
canSendMessage = canSendMessage,
|
||||||
|
canSendReaction = canSendReaction,
|
||||||
|
canPinUnpin = canPinUnpin,
|
||||||
|
)
|
||||||
|
|
||||||
fun aReactionSummaryState(
|
fun aReactionSummaryState(
|
||||||
target: ReactionSummaryState.Summary? = null,
|
target: ReactionSummaryState.Summary? = null,
|
||||||
eventSink: (ReactionSummaryEvents) -> Unit = {}
|
eventSink: (ReactionSummaryEvents) -> Unit = {}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl
|
package io.element.android.features.messages.impl
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -68,6 +71,11 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsBott
|
||||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerViewDefaults
|
||||||
|
import io.element.android.features.messages.impl.timeline.FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS
|
||||||
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineView
|
import io.element.android.features.messages.impl.timeline.TimelineView
|
||||||
import io.element.android.features.messages.impl.timeline.components.JoinCallMenuItem
|
import io.element.android.features.messages.impl.timeline.components.JoinCallMenuItem
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
||||||
|
|
@ -102,11 +110,13 @@ import io.element.android.libraries.designsystem.utils.KeepScreenOn
|
||||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesView(
|
fun MessagesView(
|
||||||
|
|
@ -120,8 +130,9 @@ fun MessagesView(
|
||||||
onSendLocationClick: () -> Unit,
|
onSendLocationClick: () -> Unit,
|
||||||
onCreatePollClick: () -> Unit,
|
onCreatePollClick: () -> Unit,
|
||||||
onJoinCallClick: () -> Unit,
|
onJoinCallClick: () -> Unit,
|
||||||
|
onViewAllPinnedMessagesClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
forceJumpToBottomVisibility: Boolean = false
|
forceJumpToBottomVisibility: Boolean = false,
|
||||||
) {
|
) {
|
||||||
OnLifecycleEvent { _, event ->
|
OnLifecycleEvent { _, event ->
|
||||||
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event))
|
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event))
|
||||||
|
|
@ -154,10 +165,7 @@ fun MessagesView(
|
||||||
state.actionListState.eventSink(
|
state.actionListState.eventSink(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = event,
|
event = event,
|
||||||
canRedactOwn = state.userHasPermissionToRedactOwn,
|
userEventPermissions = state.userEventPermissions,
|
||||||
canRedactOther = state.userHasPermissionToRedactOther,
|
|
||||||
canSendMessage = state.userHasPermissionToSendMessage,
|
|
||||||
canSendReaction = state.userHasPermissionToSendReaction,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -225,6 +233,7 @@ fun MessagesView(
|
||||||
},
|
},
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
onJoinCallClick = onJoinCallClick,
|
onJoinCallClick = onJoinCallClick,
|
||||||
|
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
|
|
@ -316,6 +325,7 @@ private fun MessagesViewContent(
|
||||||
onSendLocationClick: () -> Unit,
|
onSendLocationClick: () -> Unit,
|
||||||
onCreatePollClick: () -> Unit,
|
onCreatePollClick: () -> Unit,
|
||||||
onJoinCallClick: () -> Unit,
|
onJoinCallClick: () -> Unit,
|
||||||
|
onViewAllPinnedMessagesClick: () -> Unit,
|
||||||
forceJumpToBottomVisibility: Boolean,
|
forceJumpToBottomVisibility: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -373,22 +383,41 @@ private fun MessagesViewContent(
|
||||||
RectangleShape
|
RectangleShape
|
||||||
},
|
},
|
||||||
content = { paddingValues ->
|
content = { paddingValues ->
|
||||||
TimelineView(
|
Box(modifier = Modifier.padding(paddingValues)) {
|
||||||
state = state.timelineState,
|
val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberExitOnScrollBehavior()
|
||||||
typingNotificationState = state.typingNotificationState,
|
TimelineView(
|
||||||
onUserDataClick = onUserDataClick,
|
state = state.timelineState,
|
||||||
onLinkClick = onLinkClick,
|
typingNotificationState = state.typingNotificationState,
|
||||||
onMessageClick = onMessageClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onMessageLongClick = onMessageLongClick,
|
onLinkClick = onLinkClick,
|
||||||
onSwipeToReply = onSwipeToReply,
|
onMessageClick = onMessageClick,
|
||||||
onReactionClick = onReactionClick,
|
onMessageLongClick = onMessageLongClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onSwipeToReply = onSwipeToReply,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onReactionClick = onReactionClick,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
modifier = Modifier.padding(paddingValues),
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onJoinCallClick = onJoinCallClick,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
)
|
onJoinCallClick = onJoinCallClick,
|
||||||
|
nestedScrollConnection = scrollBehavior.nestedScrollConnection,
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible,
|
||||||
|
enter = expandVertically(),
|
||||||
|
exit = shrinkVertically(),
|
||||||
|
) {
|
||||||
|
fun focusOnPinnedEvent(eventId: EventId) {
|
||||||
|
state.timelineState.eventSink(
|
||||||
|
TimelineEvents.FocusOnEvent(eventId = eventId, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PinnedMessagesBannerView(
|
||||||
|
state = state.pinnedMessagesBannerState,
|
||||||
|
onClick = ::focusOnPinnedEvent,
|
||||||
|
onViewAllClick = onViewAllPinnedMessagesClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
sheetContent = { subcomposing: Boolean ->
|
sheetContent = { subcomposing: Boolean ->
|
||||||
MessagesViewComposerBottomSheetContents(
|
MessagesViewComposerBottomSheetContents(
|
||||||
|
|
@ -408,7 +437,7 @@ private fun MessagesViewComposerBottomSheetContents(
|
||||||
subcomposing: Boolean,
|
subcomposing: Boolean,
|
||||||
state: MessagesState,
|
state: MessagesState,
|
||||||
) {
|
) {
|
||||||
if (state.userHasPermissionToSendMessage) {
|
if (state.userEventPermissions.canSendMessage) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
MentionSuggestionsPickerView(
|
MentionSuggestionsPickerView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -557,12 +586,13 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
onRoomDetailsClick = {},
|
onRoomDetailsClick = {},
|
||||||
onEventClick = { false },
|
onEventClick = { false },
|
||||||
onPreviewAttachments = {},
|
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
|
onPreviewAttachments = {},
|
||||||
onSendLocationClick = {},
|
onSendLocationClick = {},
|
||||||
onCreatePollClick = {},
|
onCreatePollClick = {},
|
||||||
onJoinCallClick = {},
|
onJoinCallClick = {},
|
||||||
|
onViewAllPinnedMessagesClick = { },
|
||||||
forceJumpToBottomVisibility = true,
|
forceJumpToBottomVisibility = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the permissions a user has in a room.
|
||||||
|
* It's dependent of the user's power level in the room.
|
||||||
|
*/
|
||||||
|
data class UserEventPermissions(
|
||||||
|
val canRedactOwn: Boolean,
|
||||||
|
val canRedactOther: Boolean,
|
||||||
|
val canSendMessage: Boolean,
|
||||||
|
val canSendReaction: Boolean,
|
||||||
|
val canPinUnpin: Boolean,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val DEFAULT = UserEventPermissions(
|
||||||
|
canRedactOwn = false,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,15 +16,13 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.actionlist
|
package io.element.android.features.messages.impl.actionlist
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.UserEventPermissions
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
||||||
sealed interface ActionListEvents {
|
sealed interface ActionListEvents {
|
||||||
data object Clear : ActionListEvents
|
data object Clear : ActionListEvents
|
||||||
data class ComputeForMessage(
|
data class ComputeForMessage(
|
||||||
val event: TimelineItem.Event,
|
val event: TimelineItem.Event,
|
||||||
val canRedactOwn: Boolean,
|
val userEventPermissions: UserEventPermissions,
|
||||||
val canRedactOther: Boolean,
|
|
||||||
val canSendMessage: Boolean,
|
|
||||||
val canSendReaction: Boolean,
|
|
||||||
) : ActionListEvents
|
) : ActionListEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,25 +23,36 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import io.element.android.features.messages.impl.UserEventPermissions
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
|
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canReact
|
import io.element.android.features.messages.impl.timeline.model.event.canReact
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ActionListPresenter @Inject constructor(
|
class ActionListPresenter @Inject constructor(
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
|
private val featureFlagsService: FeatureFlagService,
|
||||||
|
private val room: MatrixRoom,
|
||||||
) : Presenter<ActionListState> {
|
) : Presenter<ActionListState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ActionListState {
|
override fun present(): ActionListState {
|
||||||
|
|
@ -52,17 +63,20 @@ class ActionListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val isDeveloperModeEnabled by appPreferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
|
val isDeveloperModeEnabled by appPreferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
|
||||||
|
val isPinnedEventsEnabled by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false)
|
||||||
|
val pinnedEventIds by remember {
|
||||||
|
room.roomInfoFlow.map { it.pinnedEventIds }
|
||||||
|
}.collectAsState(initial = persistentListOf())
|
||||||
|
|
||||||
fun handleEvents(event: ActionListEvents) {
|
fun handleEvents(event: ActionListEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
ActionListEvents.Clear -> target.value = ActionListState.Target.None
|
ActionListEvents.Clear -> target.value = ActionListState.Target.None
|
||||||
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
|
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
|
||||||
timelineItem = event.event,
|
timelineItem = event.event,
|
||||||
userCanRedactOwn = event.canRedactOwn,
|
usersEventPermissions = event.userEventPermissions,
|
||||||
userCanRedactOther = event.canRedactOther,
|
|
||||||
userCanSendMessage = event.canSendMessage,
|
|
||||||
userCanSendReaction = event.canSendReaction,
|
|
||||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||||
|
isPinnedEventsEnabled = isPinnedEventsEnabled,
|
||||||
|
pinnedEventIds = pinnedEventIds,
|
||||||
target = target,
|
target = target,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -76,136 +90,22 @@ class ActionListPresenter @Inject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.computeForMessage(
|
private fun CoroutineScope.computeForMessage(
|
||||||
timelineItem: TimelineItem.Event,
|
timelineItem: TimelineItem.Event,
|
||||||
userCanRedactOwn: Boolean,
|
usersEventPermissions: UserEventPermissions,
|
||||||
userCanRedactOther: Boolean,
|
|
||||||
userCanSendMessage: Boolean,
|
|
||||||
userCanSendReaction: Boolean,
|
|
||||||
isDeveloperModeEnabled: Boolean,
|
isDeveloperModeEnabled: Boolean,
|
||||||
|
isPinnedEventsEnabled: Boolean,
|
||||||
|
pinnedEventIds: ImmutableList<EventId>,
|
||||||
target: MutableState<ActionListState.Target>
|
target: MutableState<ActionListState.Target>
|
||||||
) = launch {
|
) = launch {
|
||||||
target.value = ActionListState.Target.Loading(timelineItem)
|
target.value = ActionListState.Target.Loading(timelineItem)
|
||||||
val canRedact = timelineItem.isMine && userCanRedactOwn || !timelineItem.isMine && userCanRedactOther
|
|
||||||
val actions =
|
val actions = buildActions(
|
||||||
when (timelineItem.content) {
|
timelineItem = timelineItem,
|
||||||
is TimelineItemCallNotifyContent -> {
|
usersEventPermissions = usersEventPermissions,
|
||||||
if (isDeveloperModeEnabled) {
|
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||||
listOf(TimelineItemAction.ViewSource)
|
isPinnedEventsEnabled = isPinnedEventsEnabled,
|
||||||
} else {
|
isEventPinned = pinnedEventIds.contains(timelineItem.eventId),
|
||||||
emptyList()
|
)
|
||||||
}
|
val displayEmojiReactions = usersEventPermissions.canSendReaction &&
|
||||||
}
|
|
||||||
is TimelineItemRedactedContent -> {
|
|
||||||
if (isDeveloperModeEnabled) {
|
|
||||||
listOf(TimelineItemAction.ViewSource)
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is TimelineItemStateContent -> {
|
|
||||||
buildList {
|
|
||||||
add(TimelineItemAction.Copy)
|
|
||||||
if (timelineItem.isRemote) {
|
|
||||||
add(TimelineItemAction.CopyLink)
|
|
||||||
}
|
|
||||||
if (isDeveloperModeEnabled) {
|
|
||||||
add(TimelineItemAction.ViewSource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is TimelineItemPollContent -> {
|
|
||||||
val canEndPoll = timelineItem.isRemote &&
|
|
||||||
!timelineItem.content.isEnded &&
|
|
||||||
(timelineItem.isMine || canRedact)
|
|
||||||
buildList {
|
|
||||||
if (timelineItem.isRemote) {
|
|
||||||
// Can only reply or forward messages already uploaded to the server
|
|
||||||
add(TimelineItemAction.Reply)
|
|
||||||
}
|
|
||||||
if (timelineItem.isRemote && timelineItem.isEditable) {
|
|
||||||
add(TimelineItemAction.Edit)
|
|
||||||
}
|
|
||||||
if (canEndPoll) {
|
|
||||||
add(TimelineItemAction.EndPoll)
|
|
||||||
}
|
|
||||||
if (timelineItem.content.canBeCopied()) {
|
|
||||||
add(TimelineItemAction.Copy)
|
|
||||||
}
|
|
||||||
if (timelineItem.isRemote) {
|
|
||||||
add(TimelineItemAction.CopyLink)
|
|
||||||
}
|
|
||||||
if (isDeveloperModeEnabled) {
|
|
||||||
add(TimelineItemAction.ViewSource)
|
|
||||||
}
|
|
||||||
if (!timelineItem.isMine) {
|
|
||||||
add(TimelineItemAction.ReportContent)
|
|
||||||
}
|
|
||||||
if (canRedact) {
|
|
||||||
add(TimelineItemAction.Redact)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is TimelineItemVoiceContent -> {
|
|
||||||
buildList {
|
|
||||||
if (timelineItem.isRemote) {
|
|
||||||
add(TimelineItemAction.Reply)
|
|
||||||
add(TimelineItemAction.Forward)
|
|
||||||
add(TimelineItemAction.CopyLink)
|
|
||||||
}
|
|
||||||
if (isDeveloperModeEnabled) {
|
|
||||||
add(TimelineItemAction.ViewSource)
|
|
||||||
}
|
|
||||||
if (!timelineItem.isMine) {
|
|
||||||
add(TimelineItemAction.ReportContent)
|
|
||||||
}
|
|
||||||
if (canRedact) {
|
|
||||||
add(TimelineItemAction.Redact)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is TimelineItemLegacyCallInviteContent -> {
|
|
||||||
buildList {
|
|
||||||
if (isDeveloperModeEnabled) {
|
|
||||||
add(TimelineItemAction.ViewSource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> buildList<TimelineItemAction> {
|
|
||||||
if (timelineItem.isRemote) {
|
|
||||||
// Can only reply or forward messages already uploaded to the server
|
|
||||||
if (userCanSendMessage) {
|
|
||||||
if (timelineItem.isThreaded) {
|
|
||||||
add(TimelineItemAction.ReplyInThread)
|
|
||||||
} else {
|
|
||||||
add(TimelineItemAction.Reply)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Stickers can't be forwarded (yet) so we don't show the option
|
|
||||||
// See https://github.com/element-hq/element-x-android/issues/2161
|
|
||||||
if (!timelineItem.isSticker) {
|
|
||||||
add(TimelineItemAction.Forward)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (timelineItem.isEditable) {
|
|
||||||
add(TimelineItemAction.Edit)
|
|
||||||
}
|
|
||||||
if (timelineItem.content.canBeCopied()) {
|
|
||||||
add(TimelineItemAction.Copy)
|
|
||||||
}
|
|
||||||
if (timelineItem.isRemote) {
|
|
||||||
add(TimelineItemAction.CopyLink)
|
|
||||||
}
|
|
||||||
if (isDeveloperModeEnabled) {
|
|
||||||
add(TimelineItemAction.ViewSource)
|
|
||||||
}
|
|
||||||
if (!timelineItem.isMine) {
|
|
||||||
add(TimelineItemAction.ReportContent)
|
|
||||||
}
|
|
||||||
if (canRedact) {
|
|
||||||
add(TimelineItemAction.Redact)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val displayEmojiReactions = userCanSendReaction &&
|
|
||||||
timelineItem.isRemote &&
|
timelineItem.isRemote &&
|
||||||
timelineItem.content.canReact()
|
timelineItem.content.canReact()
|
||||||
if (actions.isNotEmpty() || displayEmojiReactions) {
|
if (actions.isNotEmpty() || displayEmojiReactions) {
|
||||||
|
|
@ -219,3 +119,71 @@ class ActionListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildActions(
|
||||||
|
timelineItem: TimelineItem.Event,
|
||||||
|
usersEventPermissions: UserEventPermissions,
|
||||||
|
isDeveloperModeEnabled: Boolean,
|
||||||
|
isPinnedEventsEnabled: Boolean,
|
||||||
|
isEventPinned: Boolean,
|
||||||
|
): List<TimelineItemAction> {
|
||||||
|
val canRedact = timelineItem.isMine && usersEventPermissions.canRedactOwn || !timelineItem.isMine && usersEventPermissions.canRedactOther
|
||||||
|
return buildList {
|
||||||
|
if (timelineItem.canBeRepliedTo && usersEventPermissions.canSendMessage) {
|
||||||
|
if (timelineItem.isThreaded) {
|
||||||
|
add(TimelineItemAction.ReplyInThread)
|
||||||
|
} else {
|
||||||
|
add(TimelineItemAction.Reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (timelineItem.isRemote && timelineItem.content.canBeForwarded()) {
|
||||||
|
add(TimelineItemAction.Forward)
|
||||||
|
}
|
||||||
|
if (timelineItem.isEditable) {
|
||||||
|
add(TimelineItemAction.Edit)
|
||||||
|
}
|
||||||
|
if (canRedact && timelineItem.content is TimelineItemPollContent && !timelineItem.content.isEnded) {
|
||||||
|
add(TimelineItemAction.EndPoll)
|
||||||
|
}
|
||||||
|
val canPinUnpin = isPinnedEventsEnabled && usersEventPermissions.canPinUnpin && timelineItem.isRemote
|
||||||
|
if (canPinUnpin) {
|
||||||
|
if (isEventPinned) {
|
||||||
|
add(TimelineItemAction.Unpin)
|
||||||
|
} else {
|
||||||
|
add(TimelineItemAction.Pin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (timelineItem.content.canBeCopied()) {
|
||||||
|
add(TimelineItemAction.Copy)
|
||||||
|
}
|
||||||
|
if (timelineItem.isRemote) {
|
||||||
|
add(TimelineItemAction.CopyLink)
|
||||||
|
}
|
||||||
|
if (isDeveloperModeEnabled) {
|
||||||
|
add(TimelineItemAction.ViewSource)
|
||||||
|
}
|
||||||
|
if (!timelineItem.isMine) {
|
||||||
|
add(TimelineItemAction.ReportContent)
|
||||||
|
}
|
||||||
|
if (canRedact) {
|
||||||
|
add(TimelineItemAction.Redact)
|
||||||
|
}
|
||||||
|
}.postFilter(timelineItem.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post filter the actions based on the content of the event.
|
||||||
|
*/
|
||||||
|
private fun List<TimelineItemAction>.postFilter(content: TimelineItemEventContent): List<TimelineItemAction> {
|
||||||
|
return filter { action ->
|
||||||
|
when (content) {
|
||||||
|
is TimelineItemCallNotifyContent,
|
||||||
|
is TimelineItemLegacyCallInviteContent,
|
||||||
|
is TimelineItemStateContent,
|
||||||
|
is TimelineItemRedactedContent -> {
|
||||||
|
action == TimelineItemAction.ViewSource
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@ private fun SheetContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MultipleEmitters") // False positive
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) {
|
private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) {
|
||||||
val content: @Composable () -> Unit
|
val content: @Composable () -> Unit
|
||||||
|
|
|
||||||
|
|
@ -39,4 +39,8 @@ sealed class TimelineItemAction(
|
||||||
data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_developer_options)
|
data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_developer_options)
|
||||||
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true)
|
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true)
|
||||||
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end)
|
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end)
|
||||||
|
data object Pin : TimelineItemAction(CommonStrings.action_pin, CompoundDrawables.ic_compound_pin)
|
||||||
|
|
||||||
|
// TODO use the Unpin compound icon when available.
|
||||||
|
data object Unpin : TimelineItemAction(CommonStrings.action_unpin, CompoundDrawables.ic_compound_pin)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.di
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
|
||||||
|
@ContributesTo(RoomScope::class)
|
||||||
|
@Module
|
||||||
|
interface MessagesModule {
|
||||||
|
@Binds
|
||||||
|
fun bindPinnedMessagesBannerPresenter(presenter: PinnedMessagesBannerPresenter): Presenter<PinnedMessagesBannerState>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.pinned
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
fun interface IsPinnedMessagesFeatureEnabled {
|
||||||
|
@Composable
|
||||||
|
operator fun invoke(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultIsPinnedMessagesFeatureEnabled @Inject constructor(
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
) : IsPinnedMessagesFeatureEnabled {
|
||||||
|
@Composable
|
||||||
|
override operator fun invoke(): Boolean {
|
||||||
|
var isFeatureEnabled by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents)
|
||||||
|
.onEach { isFeatureEnabled = it }
|
||||||
|
.launchIn(this)
|
||||||
|
}
|
||||||
|
return isFeatureEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.lockscreen.impl.unlock.signout
|
package io.element.android.features.messages.impl.pinned.banner
|
||||||
|
|
||||||
interface SignOut {
|
sealed interface PinnedMessagesBannerEvents {
|
||||||
suspend operator fun invoke(): String?
|
data object MoveToNextPinned : PinnedMessagesBannerEvents
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.pinned.banner
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
|
data class PinnedMessagesBannerItem(
|
||||||
|
val eventId: EventId,
|
||||||
|
val formatted: AnnotatedString,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.pinned.banner
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class PinnedMessagesBannerItemFactory @Inject constructor(
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
private val formatter: PinnedMessagesBannerFormatter,
|
||||||
|
) {
|
||||||
|
suspend fun create(timelineItem: MatrixTimelineItem): PinnedMessagesBannerItem? = withContext(coroutineDispatchers.computation) {
|
||||||
|
when (timelineItem) {
|
||||||
|
is MatrixTimelineItem.Event -> {
|
||||||
|
val eventId = timelineItem.eventId ?: return@withContext null
|
||||||
|
val formatted = formatter.format(timelineItem.event)
|
||||||
|
PinnedMessagesBannerItem(
|
||||||
|
eventId = eventId,
|
||||||
|
formatted = if (formatted is AnnotatedString) {
|
||||||
|
formatted
|
||||||
|
} else {
|
||||||
|
AnnotatedString(formatted.toString())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.pinned.banner
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.features.messages.impl.pinned.IsPinnedMessagesFeatureEnabled
|
||||||
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
class PinnedMessagesBannerPresenter @Inject constructor(
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
private val itemFactory: PinnedMessagesBannerItemFactory,
|
||||||
|
private val isFeatureEnabled: IsPinnedMessagesFeatureEnabled,
|
||||||
|
private val networkMonitor: NetworkMonitor,
|
||||||
|
) : Presenter<PinnedMessagesBannerState> {
|
||||||
|
private val pinnedItems = mutableStateOf<ImmutableList<PinnedMessagesBannerItem>>(persistentListOf())
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): PinnedMessagesBannerState {
|
||||||
|
val isFeatureEnabled = isFeatureEnabled()
|
||||||
|
val expectedPinnedMessagesCount by remember {
|
||||||
|
room.roomInfoFlow.map { roomInfo -> roomInfo.pinnedEventIds.size }
|
||||||
|
}.collectAsState(initial = 0)
|
||||||
|
|
||||||
|
var hasTimelineFailedToLoad by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(-1) }
|
||||||
|
|
||||||
|
PinnedMessagesBannerItemsEffect(
|
||||||
|
isFeatureEnabled = isFeatureEnabled,
|
||||||
|
onItemsChange = { newItems ->
|
||||||
|
val pinnedMessageCount = newItems.size
|
||||||
|
if (currentPinnedMessageIndex >= pinnedMessageCount || currentPinnedMessageIndex < 0) {
|
||||||
|
currentPinnedMessageIndex = pinnedMessageCount - 1
|
||||||
|
}
|
||||||
|
pinnedItems.value = newItems
|
||||||
|
},
|
||||||
|
onTimelineFail = { hasTimelineFailed ->
|
||||||
|
hasTimelineFailedToLoad = hasTimelineFailed
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
fun handleEvent(event: PinnedMessagesBannerEvents) {
|
||||||
|
when (event) {
|
||||||
|
is PinnedMessagesBannerEvents.MoveToNextPinned -> {
|
||||||
|
currentPinnedMessageIndex = (currentPinnedMessageIndex - 1).mod(pinnedItems.value.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pinnedMessagesBannerState(
|
||||||
|
isFeatureEnabled = isFeatureEnabled,
|
||||||
|
hasTimelineFailed = hasTimelineFailedToLoad,
|
||||||
|
expectedPinnedMessagesCount = expectedPinnedMessagesCount,
|
||||||
|
pinnedItems = pinnedItems.value,
|
||||||
|
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||||
|
eventSink = ::handleEvent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun pinnedMessagesBannerState(
|
||||||
|
isFeatureEnabled: Boolean,
|
||||||
|
hasTimelineFailed: Boolean,
|
||||||
|
expectedPinnedMessagesCount: Int,
|
||||||
|
pinnedItems: ImmutableList<PinnedMessagesBannerItem>,
|
||||||
|
currentPinnedMessageIndex: Int,
|
||||||
|
eventSink: (PinnedMessagesBannerEvents) -> Unit
|
||||||
|
): PinnedMessagesBannerState {
|
||||||
|
val currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex)
|
||||||
|
return when {
|
||||||
|
!isFeatureEnabled -> PinnedMessagesBannerState.Hidden
|
||||||
|
hasTimelineFailed -> PinnedMessagesBannerState.Hidden
|
||||||
|
currentPinnedMessage != null -> PinnedMessagesBannerState.Loaded(
|
||||||
|
currentPinnedMessage = currentPinnedMessage,
|
||||||
|
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||||
|
loadedPinnedMessagesCount = pinnedItems.size,
|
||||||
|
eventSink = eventSink
|
||||||
|
)
|
||||||
|
expectedPinnedMessagesCount == 0 -> PinnedMessagesBannerState.Hidden
|
||||||
|
else -> PinnedMessagesBannerState.Loading(expectedPinnedMessagesCount = expectedPinnedMessagesCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
@Composable
|
||||||
|
private fun PinnedMessagesBannerItemsEffect(
|
||||||
|
isFeatureEnabled: Boolean,
|
||||||
|
onItemsChange: (ImmutableList<PinnedMessagesBannerItem>) -> Unit,
|
||||||
|
onTimelineFail: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
|
||||||
|
val updatedOnTimelineFail by rememberUpdatedState(onTimelineFail)
|
||||||
|
val networkStatus by networkMonitor.connectivity.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(isFeatureEnabled, networkStatus) {
|
||||||
|
if (!isFeatureEnabled) {
|
||||||
|
updatedOnItemsChange(persistentListOf())
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
val pinnedEventsTimeline = room.pinnedEventsTimeline()
|
||||||
|
.onFailure { updatedOnTimelineFail(true) }
|
||||||
|
.onSuccess { updatedOnTimelineFail(false) }
|
||||||
|
.getOrNull()
|
||||||
|
?: return@LaunchedEffect
|
||||||
|
|
||||||
|
pinnedEventsTimeline.timelineItems
|
||||||
|
.debounce(300.milliseconds)
|
||||||
|
.map { timelineItems ->
|
||||||
|
timelineItems.mapNotNull { timelineItem ->
|
||||||
|
itemFactory.create(timelineItem)
|
||||||
|
}.toImmutableList()
|
||||||
|
}
|
||||||
|
.onEach { newItems ->
|
||||||
|
updatedOnItemsChange(newItems)
|
||||||
|
}
|
||||||
|
.onCompletion {
|
||||||
|
pinnedEventsTimeline.close()
|
||||||
|
}
|
||||||
|
.launchIn(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.pinned.banner
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import io.element.android.libraries.designsystem.text.toAnnotatedString
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface PinnedMessagesBannerState {
|
||||||
|
data object Hidden : PinnedMessagesBannerState
|
||||||
|
sealed interface Visible : PinnedMessagesBannerState
|
||||||
|
data class Loading(val expectedPinnedMessagesCount: Int) : Visible
|
||||||
|
data class Loaded(
|
||||||
|
val currentPinnedMessage: PinnedMessagesBannerItem,
|
||||||
|
val currentPinnedMessageIndex: Int,
|
||||||
|
val loadedPinnedMessagesCount: Int,
|
||||||
|
val eventSink: (PinnedMessagesBannerEvents) -> Unit
|
||||||
|
) : Visible
|
||||||
|
|
||||||
|
fun pinnedMessagesCount() = when (this) {
|
||||||
|
is Hidden -> 0
|
||||||
|
is Loading -> expectedPinnedMessagesCount
|
||||||
|
is Loaded -> loadedPinnedMessagesCount
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentPinnedMessageIndex() = when (this) {
|
||||||
|
is Hidden -> 0
|
||||||
|
is Loading -> expectedPinnedMessagesCount - 1
|
||||||
|
is Loaded -> currentPinnedMessageIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun formattedMessage() = when (this) {
|
||||||
|
is Hidden -> AnnotatedString("")
|
||||||
|
is Loading -> stringResource(id = CommonStrings.screen_room_pinned_banner_loading_description).toAnnotatedString()
|
||||||
|
is Loaded -> currentPinnedMessage.formatted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.pinned.banner
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider<PinnedMessagesBannerState> {
|
||||||
|
override val values: Sequence<PinnedMessagesBannerState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aHiddenPinnedMessagesBannerState(),
|
||||||
|
aLoadingPinnedMessagesBannerState(knownPinnedMessagesCount = 1),
|
||||||
|
aLoadingPinnedMessagesBannerState(knownPinnedMessagesCount = 4),
|
||||||
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 1, currentPinnedMessageIndex = 0),
|
||||||
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 2, currentPinnedMessageIndex = 0),
|
||||||
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 3, currentPinnedMessageIndex = 0),
|
||||||
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 0),
|
||||||
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 1),
|
||||||
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 2),
|
||||||
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 3),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun aHiddenPinnedMessagesBannerState() = PinnedMessagesBannerState.Hidden
|
||||||
|
|
||||||
|
internal fun aLoadingPinnedMessagesBannerState(
|
||||||
|
knownPinnedMessagesCount: Int = 4
|
||||||
|
) = PinnedMessagesBannerState.Loading(
|
||||||
|
expectedPinnedMessagesCount = knownPinnedMessagesCount
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun aLoadedPinnedMessagesBannerState(
|
||||||
|
currentPinnedMessageIndex: Int = 0,
|
||||||
|
knownPinnedMessagesCount: Int = 1,
|
||||||
|
currentPinnedMessage: PinnedMessagesBannerItem = PinnedMessagesBannerItem(
|
||||||
|
eventId = EventId("\$" + Random.nextInt().toString()),
|
||||||
|
formatted = AnnotatedString("This is a pinned message")
|
||||||
|
),
|
||||||
|
eventSink: (PinnedMessagesBannerEvents) -> Unit = {}
|
||||||
|
) = PinnedMessagesBannerState.Loaded(
|
||||||
|
currentPinnedMessage = currentPinnedMessage,
|
||||||
|
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||||
|
loadedPinnedMessagesCount = knownPinnedMessagesCount,
|
||||||
|
eventSink = eventSink
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,289 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.pinned.banner
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.pinnedMessageBannerBorder
|
||||||
|
import io.element.android.libraries.designsystem.theme.pinnedMessageBannerIndicator
|
||||||
|
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PinnedMessagesBannerView(
|
||||||
|
state: PinnedMessagesBannerState,
|
||||||
|
onClick: (EventId) -> Unit,
|
||||||
|
onViewAllClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
when (state) {
|
||||||
|
PinnedMessagesBannerState.Hidden -> Unit
|
||||||
|
is PinnedMessagesBannerState.Visible -> {
|
||||||
|
PinnedMessagesBannerRow(
|
||||||
|
state = state,
|
||||||
|
onClick = onClick,
|
||||||
|
onViewAllClick = onViewAllClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PinnedMessagesBannerRow(
|
||||||
|
state: PinnedMessagesBannerState,
|
||||||
|
onClick: (EventId) -> Unit,
|
||||||
|
onViewAllClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val borderColor = ElementTheme.colors.pinnedMessageBannerBorder
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.background(color = ElementTheme.colors.bgCanvasDefault)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.drawBorder(borderColor)
|
||||||
|
.heightIn(min = 64.dp)
|
||||||
|
.clickable {
|
||||||
|
if (state is PinnedMessagesBannerState.Loaded) {
|
||||||
|
onClick(state.currentPinnedMessage.eventId)
|
||||||
|
state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
PinIndicators(
|
||||||
|
pinIndex = state.currentPinnedMessageIndex(),
|
||||||
|
pinsCount = state.pinnedMessagesCount(),
|
||||||
|
modifier = Modifier.heightIn(max = 40.dp)
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.PinSolid(),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = ElementTheme.materialColors.secondary,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
PinnedMessageItem(
|
||||||
|
index = state.currentPinnedMessageIndex(),
|
||||||
|
totalCount = state.pinnedMessagesCount(),
|
||||||
|
message = state.formattedMessage(),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
ViewAllButton(state, onViewAllClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ViewAllButton(
|
||||||
|
state: PinnedMessagesBannerState,
|
||||||
|
onViewAllClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
val text = if (state is PinnedMessagesBannerState.Loaded) {
|
||||||
|
stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
text = text,
|
||||||
|
showProgress = state is PinnedMessagesBannerState.Loading,
|
||||||
|
onClick = onViewAllClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Modifier.drawBorder(borderColor: Color): Modifier {
|
||||||
|
return this
|
||||||
|
.drawBehind {
|
||||||
|
val strokeWidth = 0.5.dp.toPx()
|
||||||
|
val y = size.height - strokeWidth / 2
|
||||||
|
drawLine(
|
||||||
|
borderColor,
|
||||||
|
Offset(0f, y),
|
||||||
|
Offset(size.width, y),
|
||||||
|
strokeWidth
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
borderColor,
|
||||||
|
Offset(0f, 0f),
|
||||||
|
Offset(size.width, 0f),
|
||||||
|
strokeWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.shadow(elevation = 5.dp, spotColor = Color.Transparent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PinIndicators(
|
||||||
|
pinIndex: Int,
|
||||||
|
pinsCount: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val indicatorHeight = remember(pinsCount) {
|
||||||
|
when (pinsCount) {
|
||||||
|
0 -> 0
|
||||||
|
1 -> 32
|
||||||
|
2 -> 18
|
||||||
|
else -> 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
LaunchedEffect(pinIndex) {
|
||||||
|
val viewportSize = lazyListState.layoutInfo.viewportSize
|
||||||
|
lazyListState.animateScrollToItem(
|
||||||
|
pinIndex,
|
||||||
|
indicatorHeight / 2 - viewportSize.height / 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier,
|
||||||
|
state = lazyListState,
|
||||||
|
verticalArrangement = spacedBy(2.dp),
|
||||||
|
userScrollEnabled = false,
|
||||||
|
) {
|
||||||
|
items(pinsCount) { index ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(2.dp)
|
||||||
|
.height(indicatorHeight.dp)
|
||||||
|
.background(
|
||||||
|
color = if (index == pinIndex) {
|
||||||
|
ElementTheme.colors.iconAccentPrimary
|
||||||
|
} else {
|
||||||
|
ElementTheme.colors.pinnedMessageBannerIndicator
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PinnedMessageItem(
|
||||||
|
index: Int,
|
||||||
|
totalCount: Int,
|
||||||
|
message: AnnotatedString?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val countMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator, index + 1, totalCount)
|
||||||
|
val fullCountMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator_description, countMessage)
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
AnimatedVisibility(totalCount > 1) {
|
||||||
|
Text(
|
||||||
|
text = annotatedTextWithBold(
|
||||||
|
text = fullCountMessage,
|
||||||
|
boldText = countMessage,
|
||||||
|
),
|
||||||
|
style = ElementTheme.typography.fontBodySmMedium,
|
||||||
|
color = ElementTheme.colors.textActionAccent,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (message != null) {
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
internal interface PinnedMessagesBannerViewScrollBehavior {
|
||||||
|
val isVisible: Boolean
|
||||||
|
val nestedScrollConnection: NestedScrollConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object PinnedMessagesBannerViewDefaults {
|
||||||
|
@Composable
|
||||||
|
fun rememberExitOnScrollBehavior(): PinnedMessagesBannerViewScrollBehavior = remember {
|
||||||
|
ExitOnScrollBehavior()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExitOnScrollBehavior : PinnedMessagesBannerViewScrollBehavior {
|
||||||
|
override var isVisible by mutableStateOf(true)
|
||||||
|
override val nestedScrollConnection: NestedScrollConnection = object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
if (available.y < -1) {
|
||||||
|
isVisible = true
|
||||||
|
}
|
||||||
|
if (available.y > 1) {
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun PinnedMessagesBannerViewPreview(@PreviewParameter(PinnedMessagesBannerStateProvider::class) state: PinnedMessagesBannerState) = ElementPreview {
|
||||||
|
PinnedMessagesBannerView(
|
||||||
|
state = state,
|
||||||
|
onClick = {},
|
||||||
|
onViewAllClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -18,10 +18,11 @@ package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
sealed interface TimelineEvents {
|
sealed interface TimelineEvents {
|
||||||
data class OnScrollFinished(val firstIndex: Int) : TimelineEvents
|
data class OnScrollFinished(val firstIndex: Int) : TimelineEvents
|
||||||
data class FocusOnEvent(val eventId: EventId) : TimelineEvents
|
data class FocusOnEvent(val eventId: EventId, val debounce: Duration = Duration.ZERO) : TimelineEvents
|
||||||
data object ClearFocusRequestState : TimelineEvents
|
data object ClearFocusRequestState : TimelineEvents
|
||||||
data object OnFocusEventRender : TimelineEvents
|
data object OnFocusEventRender : TimelineEvents
|
||||||
data object JumpToLive : TimelineEvents
|
data object JumpToLive : TimelineEvents
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,15 @@ import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
const val FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS = 200L
|
||||||
|
|
||||||
class TimelinePresenter @AssistedInject constructor(
|
class TimelinePresenter @AssistedInject constructor(
|
||||||
private val timelineItemsFactory: TimelineItemsFactory,
|
private val timelineItemsFactory: TimelineItemsFactory,
|
||||||
private val timelineItemIndexer: TimelineItemIndexer,
|
private val timelineItemIndexer: TimelineItemIndexer,
|
||||||
|
|
@ -136,13 +139,8 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
is TimelineEvents.EditPoll -> {
|
is TimelineEvents.EditPoll -> {
|
||||||
navigator.onEditPollClick(event.pollStartId)
|
navigator.onEditPollClick(event.pollStartId)
|
||||||
}
|
}
|
||||||
is TimelineEvents.FocusOnEvent -> localScope.launch {
|
is TimelineEvents.FocusOnEvent -> {
|
||||||
if (timelineItemIndexer.isKnown(event.eventId)) {
|
focusRequestState.value = FocusRequestState.Requested(event.eventId, event.debounce)
|
||||||
val index = timelineItemIndexer.indexOf(event.eventId)
|
|
||||||
focusRequestState.value = FocusRequestState.Success(eventId = event.eventId, index = index)
|
|
||||||
} else {
|
|
||||||
focusRequestState.value = FocusRequestState.Loading(eventId = event.eventId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is TimelineEvents.OnFocusEventRender -> {
|
is TimelineEvents.OnFocusEventRender -> {
|
||||||
focusRequestState.value = focusRequestState.value.onFocusEventRender()
|
focusRequestState.value = focusRequestState.value.onFocusEventRender()
|
||||||
|
|
@ -157,18 +155,29 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(focusRequestState.value) {
|
LaunchedEffect(focusRequestState.value) {
|
||||||
val currentFocusRequestState = focusRequestState.value
|
when (val currentFocusRequestState = focusRequestState.value) {
|
||||||
if (currentFocusRequestState is FocusRequestState.Loading) {
|
is FocusRequestState.Requested -> {
|
||||||
val eventId = currentFocusRequestState.eventId
|
delay(currentFocusRequestState.debounce)
|
||||||
timelineController.focusOnEvent(eventId)
|
if (timelineItemIndexer.isKnown(currentFocusRequestState.eventId)) {
|
||||||
.fold(
|
val index = timelineItemIndexer.indexOf(currentFocusRequestState.eventId)
|
||||||
onSuccess = {
|
focusRequestState.value = FocusRequestState.Success(eventId = currentFocusRequestState.eventId, index = index)
|
||||||
focusRequestState.value = FocusRequestState.Success(eventId = eventId)
|
} else {
|
||||||
},
|
focusRequestState.value = FocusRequestState.Loading(eventId = currentFocusRequestState.eventId)
|
||||||
onFailure = {
|
}
|
||||||
focusRequestState.value = FocusRequestState.Failure(throwable = it)
|
}
|
||||||
}
|
is FocusRequestState.Loading -> {
|
||||||
)
|
val eventId = currentFocusRequestState.eventId
|
||||||
|
timelineController.focusOnEvent(eventId)
|
||||||
|
.fold(
|
||||||
|
onSuccess = {
|
||||||
|
focusRequestState.value = FocusRequestState.Success(eventId = eventId)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
focusRequestState.value = FocusRequestState.Failure(throwable = it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class TimelineState(
|
data class TimelineState(
|
||||||
|
|
@ -39,6 +40,7 @@ data class TimelineState(
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface FocusRequestState {
|
sealed interface FocusRequestState {
|
||||||
data object None : FocusRequestState
|
data object None : FocusRequestState
|
||||||
|
data class Requested(val eventId: EventId, val debounce: Duration) : FocusRequestState
|
||||||
data class Loading(val eventId: EventId) : FocusRequestState
|
data class Loading(val eventId: EventId) : FocusRequestState
|
||||||
data class Success(
|
data class Success(
|
||||||
val eventId: EventId,
|
val eventId: EventId,
|
||||||
|
|
@ -54,6 +56,7 @@ sealed interface FocusRequestState {
|
||||||
|
|
||||||
fun eventId(): EventId? {
|
fun eventId(): EventId? {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
|
is Requested -> eventId
|
||||||
is Loading -> eventId
|
is Loading -> eventId
|
||||||
is Success -> eventId
|
is Success -> eventId
|
||||||
else -> null
|
else -> null
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ internal fun aTimelineItemEvent(
|
||||||
transactionId: TransactionId? = null,
|
transactionId: TransactionId? = null,
|
||||||
isMine: Boolean = false,
|
isMine: Boolean = false,
|
||||||
isEditable: Boolean = false,
|
isEditable: Boolean = false,
|
||||||
|
canBeRepliedTo: Boolean = false,
|
||||||
senderDisplayName: String = "Sender",
|
senderDisplayName: String = "Sender",
|
||||||
displayNameAmbiguous: Boolean = false,
|
displayNameAmbiguous: Boolean = false,
|
||||||
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
||||||
|
|
@ -152,6 +153,7 @@ internal fun aTimelineItemEvent(
|
||||||
sentTime = "12:34",
|
sentTime = "12:34",
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
isEditable = isEditable,
|
isEditable = isEditable,
|
||||||
|
canBeRepliedTo = canBeRepliedTo,
|
||||||
senderProfile = aProfileTimelineDetailsReady(
|
senderProfile = aProfileTimelineDetailsReady(
|
||||||
displayName = senderDisplayName,
|
displayName = senderDisplayName,
|
||||||
displayNameAmbiguous = displayNameAmbiguous,
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,10 @@ import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -90,7 +93,9 @@ fun TimelineView(
|
||||||
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
onJoinCallClick: () -> Unit,
|
onJoinCallClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
forceJumpToBottomVisibility: Boolean = false
|
lazyListState: LazyListState = rememberLazyListState(),
|
||||||
|
forceJumpToBottomVisibility: Boolean = false,
|
||||||
|
nestedScrollConnection: NestedScrollConnection = rememberNestedScrollInteropConnection(),
|
||||||
) {
|
) {
|
||||||
fun clearFocusRequestState() {
|
fun clearFocusRequestState() {
|
||||||
state.eventSink(TimelineEvents.ClearFocusRequestState)
|
state.eventSink(TimelineEvents.ClearFocusRequestState)
|
||||||
|
|
@ -109,7 +114,6 @@ fun TimelineView(
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
// Disable reverse layout when TalkBack is enabled to avoid incorrect ordering issues seen in the current Compose UI version
|
// Disable reverse layout when TalkBack is enabled to avoid incorrect ordering issues seen in the current Compose UI version
|
||||||
val useReverseLayout = remember {
|
val useReverseLayout = remember {
|
||||||
val accessibilityManager = context.getSystemService(AccessibilityManager::class.java)
|
val accessibilityManager = context.getSystemService(AccessibilityManager::class.java)
|
||||||
|
|
@ -124,7 +128,9 @@ fun TimelineView(
|
||||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.nestedScroll(nestedScrollConnection),
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
reverseLayout = useReverseLayout,
|
reverseLayout = useReverseLayout,
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,6 @@ import io.element.android.features.messages.impl.timeline.model.event.aGreyShiel
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aRedShield
|
import io.element.android.features.messages.impl.timeline.model.event.aRedShield
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
|
||||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||||
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
|
@ -153,7 +152,7 @@ fun TimelineItemEventRow(
|
||||||
} else {
|
} else {
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
}
|
}
|
||||||
val canReply = timelineRoomInfo.userHasPermissionToSendMessage && event.content.canBeRepliedTo()
|
val canReply = timelineRoomInfo.userHasPermissionToSendMessage && event.canBeRepliedTo
|
||||||
if (canReply) {
|
if (canReply) {
|
||||||
val state: SwipeableActionsState = rememberSwipeableActionsState()
|
val state: SwipeableActionsState = rememberSwipeableActionsState()
|
||||||
val offset = state.offset.floatValue
|
val offset = state.offset.floatValue
|
||||||
|
|
@ -410,6 +409,7 @@ private fun MessageSenderInformation(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MultipleEmitters") // False positive
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageEventBubbleContent(
|
private fun MessageEventBubbleContent(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ fun TimelineItemReactionsLayout(
|
||||||
}
|
}
|
||||||
val rows = rowsIn.toMutableList()
|
val rows = rowsIn.toMutableList()
|
||||||
val secondLastRow = rows[rows.size - 2].toMutableList()
|
val secondLastRow = rows[rows.size - 2].toMutableList()
|
||||||
val expandButtonPlaceable = secondLastRow.removeLast()
|
val expandButtonPlaceable = secondLastRow.removeAt(secondLastRow.lastIndex)
|
||||||
lastRow.add(0, expandButtonPlaceable)
|
lastRow.add(0, expandButtonPlaceable)
|
||||||
rows[rows.size - 2] = secondLastRow
|
rows[rows.size - 2] = secondLastRow
|
||||||
rows[rows.size - 1] = lastRow
|
rows[rows.size - 1] = lastRow
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
content = contentFactory.create(currentTimelineItem.event),
|
content = contentFactory.create(currentTimelineItem.event),
|
||||||
isMine = currentTimelineItem.event.isOwn,
|
isMine = currentTimelineItem.event.isOwn,
|
||||||
isEditable = currentTimelineItem.event.isEditable,
|
isEditable = currentTimelineItem.event.isEditable,
|
||||||
|
canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo,
|
||||||
sentTime = sentTime,
|
sentTime = sentTime,
|
||||||
groupPosition = groupPosition,
|
groupPosition = groupPosition,
|
||||||
reactionsState = currentTimelineItem.computeReactionsState(),
|
reactionsState = currentTimelineItem.computeReactionsState(),
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ sealed interface TimelineItem {
|
||||||
val sentTime: String = "",
|
val sentTime: String = "",
|
||||||
val isMine: Boolean = false,
|
val isMine: Boolean = false,
|
||||||
val isEditable: Boolean,
|
val isEditable: Boolean,
|
||||||
|
val canBeRepliedTo: Boolean,
|
||||||
val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||||
val reactionsState: TimelineItemReactions,
|
val reactionsState: TimelineItemReactions,
|
||||||
val readReceiptState: TimelineItemReadReceipts,
|
val readReceiptState: TimelineItemReadReceipts,
|
||||||
|
|
|
||||||
|
|
@ -24,27 +24,27 @@ sealed interface TimelineItemEventContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only text based content and states can be copied.
|
* Only text based content can be copied.
|
||||||
*/
|
*/
|
||||||
fun TimelineItemEventContent.canBeCopied(): Boolean =
|
fun TimelineItemEventContent.canBeCopied(): Boolean =
|
||||||
when (this) {
|
this is TimelineItemTextBasedContent
|
||||||
is TimelineItemTextBasedContent,
|
|
||||||
is TimelineItemStateContent,
|
|
||||||
is TimelineItemRedactedContent -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the event content can be replied to.
|
* Returns true if the event content can be forwarded.
|
||||||
* Note: it should match the logic in [io.element.android.features.messages.impl.actionlist.ActionListPresenter].
|
|
||||||
*/
|
*/
|
||||||
fun TimelineItemEventContent.canBeRepliedTo(): Boolean =
|
fun TimelineItemEventContent.canBeForwarded(): Boolean =
|
||||||
when (this) {
|
when (this) {
|
||||||
is TimelineItemRedactedContent,
|
is TimelineItemTextBasedContent,
|
||||||
is TimelineItemLegacyCallInviteContent,
|
is TimelineItemImageContent,
|
||||||
is TimelineItemCallNotifyContent,
|
is TimelineItemFileContent,
|
||||||
is TimelineItemStateContent -> false
|
is TimelineItemAudioContent,
|
||||||
else -> true
|
is TimelineItemVideoContent,
|
||||||
|
is TimelineItemLocationContent,
|
||||||
|
is TimelineItemVoiceContent -> true
|
||||||
|
// Stickers can't be forwarded (yet) so we don't show the option
|
||||||
|
// See https://github.com/element-hq/element-x-android/issues/2161
|
||||||
|
is TimelineItemStickerContent -> false
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,12 @@ internal fun MessagesViewWithTypingPreview(
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
onRoomDetailsClick = {},
|
onRoomDetailsClick = {},
|
||||||
onEventClick = { false },
|
onEventClick = { false },
|
||||||
onPreviewAttachments = {},
|
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
|
onPreviewAttachments = {},
|
||||||
onSendLocationClick = {},
|
onSendLocationClick = {},
|
||||||
onCreatePollClick = {},
|
onCreatePollClick = {},
|
||||||
onJoinCallClick = {},
|
onJoinCallClick = {},
|
||||||
|
onViewAllPinnedMessagesClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Suppress("MultipleEmitters") // False positive
|
||||||
@Composable
|
@Composable
|
||||||
fun TypingNotificationView(
|
fun TypingNotificationView(
|
||||||
state: TypingNotificationState,
|
state: TypingNotificationState,
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,18 @@
|
||||||
<string name="screen_room_timeline_read_marker_title">"Nowe"</string>
|
<string name="screen_room_timeline_read_marker_title">"Nowe"</string>
|
||||||
<plurals name="screen_room_timeline_state_changes">
|
<plurals name="screen_room_timeline_state_changes">
|
||||||
<item quantity="one">"%1$d zmiana pokoju"</item>
|
<item quantity="one">"%1$d zmiana pokoju"</item>
|
||||||
<item quantity="few">"%1$d zmian pokoju"</item>
|
<item quantity="few">"%1$d zmiany pokoju"</item>
|
||||||
<item quantity="many">"%1$d zmiany pokoju"</item>
|
<item quantity="many">"%1$d zmian pokoju"</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="screen_room_typing_many_members">
|
||||||
|
<item quantity="one">"%1$s, %2$s i %3$d inny"</item>
|
||||||
|
<item quantity="few">"%1$s, %2$s i %3$d innych"</item>
|
||||||
|
<item quantity="many">"%1$s, %2$s i %3$d innych"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="screen_room_typing_notification">
|
<plurals name="screen_room_typing_notification">
|
||||||
<item quantity="one">"%1$s piszę"</item>
|
<item quantity="one">"%1$s piszę"</item>
|
||||||
<item quantity="few">"%1$s piszą"</item>
|
<item quantity="few">"%1$s piszą"</item>
|
||||||
<item quantity="many">"%1$s piszą"</item>
|
<item quantity="many">"%1$s pisze"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="screen_room_typing_two_members">"%1$s i %2$s"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
<string name="screen_room_encrypted_history_banner">"O histórico de mensagens não está disponível no momento."</string>
|
<string name="screen_room_encrypted_history_banner">"O histórico de mensagens não está disponível no momento."</string>
|
||||||
<string name="screen_room_invite_again_alert_message">"Gostaria de convidá-los de volta?"</string>
|
<string name="screen_room_invite_again_alert_message">"Gostaria de convidá-los de volta?"</string>
|
||||||
<string name="screen_room_invite_again_alert_title">"Você está sozinho neste chat"</string>
|
<string name="screen_room_invite_again_alert_title">"Você está sozinho neste chat"</string>
|
||||||
|
<string name="screen_room_mentions_at_room_subtitle">"Notificar a sala inteira"</string>
|
||||||
<string name="screen_room_mentions_at_room_title">"Todos"</string>
|
<string name="screen_room_mentions_at_room_title">"Todos"</string>
|
||||||
<string name="screen_room_retry_send_menu_send_again_action">"Enviar novamente"</string>
|
<string name="screen_room_retry_send_menu_send_again_action">"Enviar novamente"</string>
|
||||||
<string name="screen_room_retry_send_menu_title">"Sua mensagem não foi enviada"</string>
|
<string name="screen_room_retry_send_menu_title">"Sua mensagem não foi enviada"</string>
|
||||||
|
|
@ -36,7 +37,16 @@
|
||||||
<string name="screen_room_timeline_reactions_show_more">"Mostrar mais"</string>
|
<string name="screen_room_timeline_reactions_show_more">"Mostrar mais"</string>
|
||||||
<string name="screen_room_timeline_read_marker_title">"Novo"</string>
|
<string name="screen_room_timeline_read_marker_title">"Novo"</string>
|
||||||
<plurals name="screen_room_timeline_state_changes">
|
<plurals name="screen_room_timeline_state_changes">
|
||||||
<item quantity="one">"%1$d mudança de sala"</item>
|
<item quantity="one">"%1$d alteração na sala"</item>
|
||||||
<item quantity="other">"%1$d mudanças de salas"</item>
|
<item quantity="other">"%1$d alterações na sala"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="screen_room_typing_many_members">
|
||||||
|
<item quantity="one">"%1$s, %2$s e %3$d outro"</item>
|
||||||
|
<item quantity="other">"%1$s, %2$s e %3$d outros"</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="screen_room_typing_notification">
|
||||||
|
<item quantity="one">"%1$s está digitando"</item>
|
||||||
|
<item quantity="other">"%1$s estão digitando"</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="screen_room_typing_two_members">"%1$s e %2$s"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="emoji_picker_category_activity">"Faoliyatlar"</string>
|
||||||
|
<string name="emoji_picker_category_flags">"Bayroqlar"</string>
|
||||||
|
<string name="emoji_picker_category_foods">"Oziq-ovqat va ichimliklar"</string>
|
||||||
|
<string name="emoji_picker_category_nature">"Hayvonlar va tabiat"</string>
|
||||||
|
<string name="emoji_picker_category_objects">"Ob\'ektlar"</string>
|
||||||
|
<string name="emoji_picker_category_people">"Smayllar va odamlar"</string>
|
||||||
|
<string name="emoji_picker_category_places">"Sayohat va Joylar"</string>
|
||||||
|
<string name="emoji_picker_category_symbols">"Belgilar"</string>
|
||||||
|
<string name="screen_report_content_block_user">"Foydalanuvchini bloklash"</string>
|
||||||
|
<string name="screen_report_content_block_user_hint">"Ushbu foydalanuvchidan barcha joriy va kelajakdagi xabarlarni yashirishni xohlayotganingizni tekshiring"</string>
|
||||||
|
<string name="screen_report_content_explanation">"Bu xabar uy serveringiz administratoriga xabar qilinadi. Ular hech qanday shifrlangan xabarlarni o\'qiy olmaydi."</string>
|
||||||
|
<string name="screen_report_content_hint">"Ushbu kontent haqida xabar berish sababi"</string>
|
||||||
|
<string name="screen_room_attachment_source_camera">"Kamera"</string>
|
||||||
|
<string name="screen_room_attachment_source_camera_photo">"Rasmga olmoq"</string>
|
||||||
|
<string name="screen_room_attachment_source_camera_video">"Video yozib olish"</string>
|
||||||
|
<string name="screen_room_attachment_source_files">"Biriktirma"</string>
|
||||||
|
<string name="screen_room_attachment_source_gallery">"Fotosurat va video kutubxonasi"</string>
|
||||||
|
<string name="screen_room_attachment_source_location">"Joylashuv"</string>
|
||||||
|
<string name="screen_room_attachment_source_poll">"So\'ro\'vnoma"</string>
|
||||||
|
<string name="screen_room_attachment_text_formatting">"Matnni formatlash"</string>
|
||||||
|
<string name="screen_room_encrypted_history_banner">"Xabarlar tarixi hozirda mavjud emas."</string>
|
||||||
|
<string name="screen_room_invite_again_alert_message">"Ularni yana taklif qilmoqchimisiz?"</string>
|
||||||
|
<string name="screen_room_invite_again_alert_title">"Siz bu chatda yolg\'izsiz"</string>
|
||||||
|
<string name="screen_room_mentions_at_room_title">"Har kim"</string>
|
||||||
|
<string name="screen_room_retry_send_menu_send_again_action">"Yana yuboring"</string>
|
||||||
|
<string name="screen_room_retry_send_menu_title">"Xabaringiz yuborilmadi"</string>
|
||||||
|
<string name="screen_room_timeline_add_reaction">"Emoji qo\'shmoq"</string>
|
||||||
|
<string name="screen_room_timeline_beginning_of_room">"Bu %1$sni boshlanishi"</string>
|
||||||
|
<string name="screen_room_timeline_beginning_of_room_no_name">"Bu suhbatning boshlanishi."</string>
|
||||||
|
<string name="screen_room_timeline_less_reactions">"Kamroq ko\'rsatish"</string>
|
||||||
|
<string name="screen_room_timeline_message_copied">"Xabar nusxalandi"</string>
|
||||||
|
<string name="screen_room_timeline_no_permission_to_post">"Sizda bu xonaga post yozishga ruxsat yo‘q"</string>
|
||||||
|
<string name="screen_room_timeline_reactions_show_less">"Kamroq ko\'rsatish"</string>
|
||||||
|
<string name="screen_room_timeline_reactions_show_more">"Ko\'proq ko\'rsatish"</string>
|
||||||
|
<string name="screen_room_timeline_read_marker_title">"Yangi"</string>
|
||||||
|
<plurals name="screen_room_timeline_state_changes">
|
||||||
|
<item quantity="one">"%1$dxonani almashtirish"</item>
|
||||||
|
<item quantity="other">"%1$dxona o\'zgarishi"</item>
|
||||||
|
</plurals>
|
||||||
|
</resources>
|
||||||
|
|
@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext
|
import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
|
||||||
import io.element.android.features.messages.impl.textcomposer.TestRichTextEditorStateFactory
|
import io.element.android.features.messages.impl.textcomposer.TestRichTextEditorStateFactory
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
||||||
|
|
@ -43,6 +44,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationPresenter
|
import io.element.android.features.messages.impl.typing.TypingNotificationPresenter
|
||||||
import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper
|
import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
|
||||||
|
|
@ -77,6 +79,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
|
|
@ -137,8 +140,8 @@ class MessagesPresenterTest {
|
||||||
assertThat(initialState.roomName).isEqualTo(AsyncData.Success(""))
|
assertThat(initialState.roomName).isEqualTo(AsyncData.Success(""))
|
||||||
assertThat(initialState.roomAvatar)
|
assertThat(initialState.roomAvatar)
|
||||||
.isEqualTo(AsyncData.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom)))
|
.isEqualTo(AsyncData.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom)))
|
||||||
assertThat(initialState.userHasPermissionToSendMessage).isTrue()
|
assertThat(initialState.userEventPermissions.canSendMessage).isTrue()
|
||||||
assertThat(initialState.userHasPermissionToRedactOwn).isTrue()
|
assertThat(initialState.userEventPermissions.canRedactOwn).isTrue()
|
||||||
assertThat(initialState.hasNetworkConnection).isTrue()
|
assertThat(initialState.hasNetworkConnection).isTrue()
|
||||||
assertThat(initialState.snackbarMessage).isNull()
|
assertThat(initialState.snackbarMessage).isNull()
|
||||||
assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized)
|
assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized)
|
||||||
|
|
@ -155,6 +158,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
assertThat(room.markAsReadCalls).isEmpty()
|
assertThat(room.markAsReadCalls).isEmpty()
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
|
|
@ -175,6 +179,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOwnResult = { Result.success(true) },
|
canRedactOwnResult = { Result.success(true) },
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
).apply {
|
).apply {
|
||||||
givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||||
}
|
}
|
||||||
|
|
@ -203,6 +208,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -240,6 +246,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -298,6 +305,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(
|
val presenter = createMessagesPresenter(
|
||||||
clipboardHelper = clipboardHelper,
|
clipboardHelper = clipboardHelper,
|
||||||
|
|
@ -487,6 +495,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
|
|
||||||
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
|
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
|
||||||
|
|
@ -561,6 +570,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -596,6 +606,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -620,6 +631,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -644,6 +656,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
room.givenRoomMembersState(
|
room.givenRoomMembersState(
|
||||||
MatrixRoomMembersState.Ready(
|
MatrixRoomMembersState.Ready(
|
||||||
|
|
@ -679,6 +692,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
room.givenRoomMembersState(
|
room.givenRoomMembersState(
|
||||||
MatrixRoomMembersState.Error(
|
MatrixRoomMembersState.Error(
|
||||||
|
|
@ -715,6 +729,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
room.givenRoomMembersState(MatrixRoomMembersState.Unknown)
|
room.givenRoomMembersState(MatrixRoomMembersState.Unknown)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
|
|
@ -741,6 +756,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
room.givenRoomMembersState(
|
room.givenRoomMembersState(
|
||||||
MatrixRoomMembersState.Ready(
|
MatrixRoomMembersState.Ready(
|
||||||
|
|
@ -781,13 +797,14 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val state = awaitFirstItem()
|
val state = awaitFirstItem()
|
||||||
assertThat(state.userHasPermissionToSendMessage).isTrue()
|
assertThat(state.userEventPermissions.canSendMessage).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -805,15 +822,16 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
// Default value
|
// Default value
|
||||||
assertThat(awaitItem().userHasPermissionToSendMessage).isTrue()
|
assertThat(awaitItem().userEventPermissions.canSendMessage).isTrue()
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
assertThat(awaitItem().userHasPermissionToSendMessage).isFalse()
|
assertThat(awaitItem().userEventPermissions.canSendMessage).isFalse()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -826,14 +844,15 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(false) },
|
canRedactOtherResult = { Result.success(false) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedactOwn }.last()
|
val initialState = consumeItemsUntilPredicate { it.userEventPermissions.canRedactOwn }.last()
|
||||||
assertThat(initialState.userHasPermissionToRedactOwn).isTrue()
|
assertThat(initialState.userEventPermissions.canRedactOwn).isTrue()
|
||||||
assertThat(initialState.userHasPermissionToRedactOther).isFalse()
|
assertThat(initialState.userEventPermissions.canRedactOther).isFalse()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -846,14 +865,15 @@ class MessagesPresenterTest {
|
||||||
canRedactOwnResult = { Result.success(false) },
|
canRedactOwnResult = { Result.success(false) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedactOther }.last()
|
val initialState = consumeItemsUntilPredicate { it.userEventPermissions.canRedactOther }.last()
|
||||||
assertThat(initialState.userHasPermissionToRedactOwn).isFalse()
|
assertThat(initialState.userEventPermissions.canRedactOwn).isFalse()
|
||||||
assertThat(initialState.userHasPermissionToRedactOther).isTrue()
|
assertThat(initialState.userEventPermissions.canRedactOther).isTrue()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -878,6 +898,74 @@ class MessagesPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action pin`() = runTest {
|
||||||
|
val successPinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
|
||||||
|
val failurePinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) }
|
||||||
|
val timeline = FakeTimeline()
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
liveTimeline = timeline,
|
||||||
|
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||||
|
canRedactOwnResult = { Result.success(true) },
|
||||||
|
canRedactOtherResult = { Result.success(true) },
|
||||||
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
|
)
|
||||||
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
content = aTimelineItemTextContent()
|
||||||
|
)
|
||||||
|
val initialState = awaitFirstItem()
|
||||||
|
|
||||||
|
timeline.pinEventLambda = successPinEventLambda
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent))
|
||||||
|
assert(successPinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
|
||||||
|
|
||||||
|
timeline.pinEventLambda = failurePinEventLambda
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent))
|
||||||
|
assert(failurePinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
|
||||||
|
assertThat(awaitItem().snackbarMessage).isNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action unpin`() = runTest {
|
||||||
|
val successUnpinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
|
||||||
|
val failureUnpinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) }
|
||||||
|
val timeline = FakeTimeline()
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
liveTimeline = timeline,
|
||||||
|
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||||
|
canRedactOwnResult = { Result.success(true) },
|
||||||
|
canRedactOtherResult = { Result.success(true) },
|
||||||
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
|
)
|
||||||
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
content = aTimelineItemTextContent()
|
||||||
|
)
|
||||||
|
val initialState = awaitFirstItem()
|
||||||
|
|
||||||
|
timeline.unpinEventLambda = successUnpinEventLambda
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent))
|
||||||
|
assert(successUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
|
||||||
|
|
||||||
|
timeline.unpinEventLambda = failureUnpinEventLambda
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent))
|
||||||
|
assert(failureUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
|
||||||
|
assertThat(awaitItem().snackbarMessage).isNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||||
// Skip 2 item if Mentions feature is enabled, else 1
|
// Skip 2 item if Mentions feature is enabled, else 1
|
||||||
skipItems(if (FeatureFlags.Mentions.defaultValue(aBuildMeta())) 2 else 1)
|
skipItems(if (FeatureFlags.Mentions.defaultValue(aBuildMeta())) 2 else 1)
|
||||||
|
|
@ -892,6 +980,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
).apply {
|
).apply {
|
||||||
givenRoomInfo(aRoomInfo(id = roomId, name = ""))
|
givenRoomInfo(aRoomInfo(id = roomId, name = ""))
|
||||||
},
|
},
|
||||||
|
|
@ -958,14 +1047,21 @@ class MessagesPresenterTest {
|
||||||
return timelinePresenter
|
return timelinePresenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val actionListPresenter = ActionListPresenter(appPreferencesStore = appPreferencesStore)
|
val featureFlagService = FakeFeatureFlagService()
|
||||||
|
val actionListPresenter = ActionListPresenter(
|
||||||
|
appPreferencesStore = appPreferencesStore,
|
||||||
|
featureFlagsService = featureFlagService,
|
||||||
|
room = matrixRoom,
|
||||||
|
)
|
||||||
val typingNotificationPresenter = TypingNotificationPresenter(
|
val typingNotificationPresenter = TypingNotificationPresenter(
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
)
|
)
|
||||||
|
|
||||||
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
||||||
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
||||||
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
||||||
|
|
||||||
return MessagesPresenter(
|
return MessagesPresenter(
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
composerPresenter = messageComposerPresenter,
|
composerPresenter = messageComposerPresenter,
|
||||||
|
|
@ -976,11 +1072,12 @@ class MessagesPresenterTest {
|
||||||
customReactionPresenter = customReactionPresenter,
|
customReactionPresenter = customReactionPresenter,
|
||||||
reactionSummaryPresenter = reactionSummaryPresenter,
|
reactionSummaryPresenter = reactionSummaryPresenter,
|
||||||
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter,
|
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter,
|
||||||
|
pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() },
|
||||||
networkMonitor = FakeNetworkMonitor(),
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
clipboardHelper = clipboardHelper,
|
clipboardHelper = clipboardHelper,
|
||||||
featureFlagsService = FakeFeatureFlagService(),
|
featureFlagsService = featureFlagService,
|
||||||
buildMeta = aBuildMeta(),
|
buildMeta = aBuildMeta(),
|
||||||
dispatchers = coroutineDispatchers,
|
dispatchers = coroutineDispatchers,
|
||||||
htmlConverterProvider = FakeHtmlConverterProvider(),
|
htmlConverterProvider = FakeHtmlConverterProvider(),
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,14 @@ import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||||
import androidx.compose.ui.test.onAllNodesWithTag
|
import androidx.compose.ui.test.onAllNodesWithTag
|
||||||
import androidx.compose.ui.test.onAllNodesWithText
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.test.onFirst
|
import androidx.compose.ui.test.onFirst
|
||||||
|
import androidx.compose.ui.test.onLast
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.performTouchInput
|
import androidx.compose.ui.test.performTouchInput
|
||||||
import androidx.compose.ui.test.swipeRight
|
import androidx.compose.ui.test.swipeRight
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.emojibasebindings.Emoji
|
import io.element.android.emojibasebindings.Emoji
|
||||||
import io.element.android.emojibasebindings.EmojibaseCategory
|
import io.element.android.emojibasebindings.EmojibaseCategory
|
||||||
|
|
@ -42,8 +44,11 @@ import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerItem
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
|
||||||
|
import io.element.android.features.messages.impl.timeline.FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS
|
||||||
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo
|
import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||||
|
|
@ -53,8 +58,8 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
|
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
|
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureCalledOnceWithParam
|
import io.element.android.tests.testutils.EnsureCalledOnceWithParam
|
||||||
|
|
@ -73,6 +78,7 @@ import org.junit.Test
|
||||||
import org.junit.rules.TestRule
|
import org.junit.rules.TestRule
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class MessagesViewTest {
|
class MessagesViewTest {
|
||||||
|
|
@ -169,16 +175,20 @@ class MessagesViewTest {
|
||||||
userHasPermissionToRedactOwn: Boolean = false,
|
userHasPermissionToRedactOwn: Boolean = false,
|
||||||
userHasPermissionToRedactOther: Boolean = false,
|
userHasPermissionToRedactOther: Boolean = false,
|
||||||
userHasPermissionToSendReaction: Boolean = false,
|
userHasPermissionToSendReaction: Boolean = false,
|
||||||
|
userCanPinEvent: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val eventsRecorder = EventsRecorder<ActionListEvents>()
|
val eventsRecorder = EventsRecorder<ActionListEvents>()
|
||||||
val state = aMessagesState(
|
val state = aMessagesState(
|
||||||
actionListState = anActionListState(
|
actionListState = anActionListState(
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
userEventPermissions = UserEventPermissions(
|
||||||
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
|
canSendMessage = userHasPermissionToSendMessage,
|
||||||
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
|
canRedactOwn = userHasPermissionToRedactOwn,
|
||||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
canRedactOther = userHasPermissionToRedactOther,
|
||||||
|
canSendReaction = userHasPermissionToSendReaction,
|
||||||
|
canPinUnpin = userCanPinEvent,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
||||||
rule.setMessagesView(
|
rule.setMessagesView(
|
||||||
|
|
@ -189,10 +199,7 @@ class MessagesViewTest {
|
||||||
eventsRecorder.assertSingle(
|
eventsRecorder.assertSingle(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
canRedactOwn = state.userHasPermissionToRedactOwn,
|
userEventPermissions = state.userEventPermissions,
|
||||||
canRedactOther = state.userHasPermissionToRedactOther,
|
|
||||||
canSendMessage = state.userHasPermissionToSendMessage,
|
|
||||||
canSendReaction = state.userHasPermissionToSendReaction,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -237,9 +244,11 @@ class MessagesViewTest {
|
||||||
|
|
||||||
private fun swipeTest(userHasPermissionToSendMessage: Boolean) {
|
private fun swipeTest(userHasPermissionToSendMessage: Boolean) {
|
||||||
val eventsRecorder = EventsRecorder<MessagesEvents>()
|
val eventsRecorder = EventsRecorder<MessagesEvents>()
|
||||||
|
val canBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = true)
|
||||||
|
val cannotBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = false)
|
||||||
val state = aMessagesState(
|
val state = aMessagesState(
|
||||||
timelineState = aTimelineState(
|
timelineState = aTimelineState(
|
||||||
timelineItems = aTimelineItemList(aTimelineItemTextContent()),
|
timelineItems = persistentListOf(canBeRepliedEvent, cannotBeRepliedEvent),
|
||||||
timelineRoomInfo = aTimelineRoomInfo(
|
timelineRoomInfo = aTimelineRoomInfo(
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage
|
userHasPermissionToSendMessage = userHasPermissionToSendMessage
|
||||||
),
|
),
|
||||||
|
|
@ -249,10 +258,12 @@ class MessagesViewTest {
|
||||||
rule.setMessagesView(
|
rule.setMessagesView(
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { swipeRight(endX = 200f) }
|
rule.onAllNodesWithTag(TestTags.messageBubble.value).apply {
|
||||||
|
onFirst().performTouchInput { swipeRight(endX = 200f) }
|
||||||
|
onLast().performTouchInput { swipeRight(endX = 200f) }
|
||||||
|
}
|
||||||
if (userHasPermissionToSendMessage) {
|
if (userHasPermissionToSendMessage) {
|
||||||
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
eventsRecorder.assertSingle(MessagesEvents.HandleAction(TimelineItemAction.Reply, canBeRepliedEvent))
|
||||||
eventsRecorder.assertSingle(MessagesEvents.HandleAction(TimelineItemAction.Reply, timelineItem))
|
|
||||||
} else {
|
} else {
|
||||||
eventsRecorder.assertEmpty()
|
eventsRecorder.assertEmpty()
|
||||||
}
|
}
|
||||||
|
|
@ -454,6 +465,25 @@ class MessagesViewTest {
|
||||||
customReactionStateEventsRecorder.assertSingle(CustomReactionEvents.DismissCustomReactionSheet)
|
customReactionStateEventsRecorder.assertSingle(CustomReactionEvents.DismissCustomReactionSheet)
|
||||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction(aUnicode, timelineItem.eventId!!))
|
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction(aUnicode, timelineItem.eventId!!))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on pinned messages banner emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<TimelineEvents>()
|
||||||
|
val state = aMessagesState(
|
||||||
|
timelineState = aTimelineState(eventSink = eventsRecorder),
|
||||||
|
pinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(
|
||||||
|
knownPinnedMessagesCount = 2,
|
||||||
|
currentPinnedMessageIndex = 0,
|
||||||
|
currentPinnedMessage = PinnedMessagesBannerItem(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
formatted = AnnotatedString("This is a pinned message")
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.setMessagesView(state = state)
|
||||||
|
rule.onNodeWithText("This is a pinned message").performClick()
|
||||||
|
eventsRecorder.assertSingle(TimelineEvents.FocusOnEvent(AN_EVENT_ID, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessagesView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessagesView(
|
||||||
|
|
@ -467,6 +497,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
|
||||||
onSendLocationClick: () -> Unit = EnsureNeverCalled(),
|
onSendLocationClick: () -> Unit = EnsureNeverCalled(),
|
||||||
onCreatePollClick: () -> Unit = EnsureNeverCalled(),
|
onCreatePollClick: () -> Unit = EnsureNeverCalled(),
|
||||||
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
|
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
// Cannot use the RichTextEditor, so simulate a LocalInspectionMode
|
// Cannot use the RichTextEditor, so simulate a LocalInspectionMode
|
||||||
|
|
@ -484,6 +515,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
|
||||||
onSendLocationClick = onSendLocationClick,
|
onSendLocationClick = onSendLocationClick,
|
||||||
onCreatePollClick = onCreatePollClick,
|
onCreatePollClick = onCreatePollClick,
|
||||||
onJoinCallClick = onJoinCallClick,
|
onJoinCallClick = onJoinCallClick,
|
||||||
|
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.messages.impl.aUserEventPermissions
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
|
|
@ -31,7 +32,13 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
||||||
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
|
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -46,7 +53,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -57,7 +64,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for message from me redacted`() = runTest {
|
fun `present - compute for message from me redacted`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -66,10 +73,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = false,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = false,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
|
|
@ -91,7 +101,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for message from others redacted`() = runTest {
|
fun `present - compute for message from others redacted`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -104,10 +114,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = false,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = false,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
|
|
@ -129,7 +142,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for others message`() = runTest {
|
fun `present - compute for others message`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -142,10 +155,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = false,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = false,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
|
|
@ -158,6 +174,7 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
|
|
@ -172,7 +189,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for others message cannot sent message`() = runTest {
|
fun `present - compute for others message cannot sent message`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -185,10 +202,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = true,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = true,
|
||||||
canSendMessage = false,
|
canRedactOther = false,
|
||||||
canSendReaction = true
|
canSendMessage = false,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
|
|
@ -200,6 +220,7 @@ class ActionListPresenterTest {
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
|
|
@ -214,7 +235,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for others message and can redact`() = runTest {
|
fun `present - compute for others message and can redact`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -227,10 +248,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = false,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = true,
|
canRedactOwn = false,
|
||||||
canSendMessage = true,
|
canRedactOther = true,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -241,6 +265,7 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
|
|
@ -256,7 +281,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for others message and cannot send reaction`() = runTest {
|
fun `present - compute for others message and cannot send reaction`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -269,10 +294,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = false,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = true,
|
canRedactOwn = false,
|
||||||
canSendMessage = true,
|
canRedactOther = true,
|
||||||
canSendReaction = false
|
canSendMessage = true,
|
||||||
|
canSendReaction = false,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -283,6 +311,7 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
|
|
@ -298,7 +327,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for my message`() = runTest {
|
fun `present - compute for my message`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -310,10 +339,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = true,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = true,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
|
|
@ -327,6 +359,7 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
|
|
@ -341,7 +374,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for my message cannot redact`() = runTest {
|
fun `present - compute for my message cannot redact`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -353,10 +386,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = false,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = false,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
|
|
@ -370,6 +406,7 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
|
|
@ -383,7 +420,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for a media item`() = runTest {
|
fun `present - compute for a media item`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -396,10 +433,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = true,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = true,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
|
|
@ -412,6 +452,7 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
|
|
@ -425,7 +466,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for a state item in debug build`() = runTest {
|
fun `present - compute for a state item in debug build`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -437,10 +478,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = stateEvent,
|
event = stateEvent,
|
||||||
canRedactOwn = false,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = false,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
|
|
@ -451,8 +495,6 @@ class ActionListPresenterTest {
|
||||||
event = stateEvent,
|
event = stateEvent,
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -464,7 +506,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for a state item in non-debuggable build`() = runTest {
|
fun `present - compute for a state item in non-debuggable build`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = false)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = false, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -476,33 +518,24 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = stateEvent,
|
event = stateEvent,
|
||||||
canRedactOwn = false,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = false,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
val successState = awaitItem()
|
|
||||||
assertThat(successState.target).isEqualTo(
|
|
||||||
ActionListState.Target.Success(
|
|
||||||
event = stateEvent,
|
|
||||||
displayEmojiReactions = false,
|
|
||||||
actions = persistentListOf(
|
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
|
||||||
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute message in non-debuggable build`() = runTest {
|
fun `present - compute message in non-debuggable build`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = false)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = false, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -514,10 +547,59 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = true,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = true,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// val loadingState = awaitItem()
|
||||||
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.target).isEqualTo(
|
||||||
|
ActionListState.Target.Success(
|
||||||
|
event = messageEvent,
|
||||||
|
displayEmojiReactions = true,
|
||||||
|
actions = persistentListOf(
|
||||||
|
TimelineItemAction.Reply,
|
||||||
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Redact,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - compute message when user can't pin`() = runTest {
|
||||||
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
isMine = true,
|
||||||
|
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
userEventPermissions = aUserEventPermissions(
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = false,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
|
|
@ -533,6 +615,61 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.ViewSource,
|
||||||
|
TimelineItemAction.Redact,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - compute message when event is already pinned`() = runTest {
|
||||||
|
val room = FakeMatrixRoom().apply {
|
||||||
|
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
|
||||||
|
}
|
||||||
|
val presenter = createActionListPresenter(
|
||||||
|
isDeveloperModeEnabled = true,
|
||||||
|
isPinFeatureEnabled = true,
|
||||||
|
room = room
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
isMine = true,
|
||||||
|
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
userEventPermissions = aUserEventPermissions(
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// val loadingState = awaitItem()
|
||||||
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.target).isEqualTo(
|
||||||
|
ActionListState.Target.Success(
|
||||||
|
event = messageEvent,
|
||||||
|
displayEmojiReactions = true,
|
||||||
|
actions = persistentListOf(
|
||||||
|
TimelineItemAction.Reply,
|
||||||
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.Unpin,
|
||||||
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -544,7 +681,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute message with no actions`() = runTest {
|
fun `present - compute message with no actions`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = false)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = false, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -561,10 +698,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = false,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = false,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
|
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
|
||||||
|
|
@ -572,10 +712,12 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = redactedEvent,
|
event = redactedEvent,
|
||||||
canRedactOwn = false,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = false,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
awaitItem().run {
|
awaitItem().run {
|
||||||
|
|
@ -586,7 +728,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute not sent message`() = runTest {
|
fun `present - compute not sent message`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = false)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = false, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -595,16 +737,20 @@ class ActionListPresenterTest {
|
||||||
// No event id, so it's not sent yet
|
// No event id, so it's not sent yet
|
||||||
eventId = null,
|
eventId = null,
|
||||||
isMine = true,
|
isMine = true,
|
||||||
|
canBeRepliedTo = false,
|
||||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null),
|
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null),
|
||||||
)
|
)
|
||||||
|
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = true,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = true,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -624,7 +770,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for editable poll message`() = runTest {
|
fun `present - compute for editable poll message`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = false)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = false, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -637,10 +783,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = true,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = true,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -652,6 +801,7 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -662,7 +812,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for non-editable poll message`() = runTest {
|
fun `present - compute for non-editable poll message`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = false)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = false, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -675,10 +825,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = true,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = true,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -689,6 +842,7 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -699,7 +853,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for ended poll message`() = runTest {
|
fun `present - compute for ended poll message`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = false)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = false, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -712,10 +866,13 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = true,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = true,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -725,6 +882,7 @@ class ActionListPresenterTest {
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -735,22 +893,26 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for voice message`() = runTest {
|
fun `present - compute for voice message`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = false)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = false, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val messageEvent = aMessageEvent(
|
val messageEvent = aMessageEvent(
|
||||||
isMine = true,
|
isMine = true,
|
||||||
|
isEditable = false,
|
||||||
content = aTimelineItemVoiceContent(),
|
content = aTimelineItemVoiceContent(),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = true,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = true,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -761,6 +923,7 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -771,7 +934,7 @@ class ActionListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for call notify`() = runTest {
|
fun `present - compute for call notify`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -783,10 +946,12 @@ class ActionListPresenterTest {
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
canRedactOwn = true,
|
userEventPermissions = aUserEventPermissions(
|
||||||
canRedactOther = false,
|
canRedactOwn = true,
|
||||||
canSendMessage = true,
|
canRedactOther = false,
|
||||||
canSendReaction = true,
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -803,7 +968,20 @@ class ActionListPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter {
|
private fun createActionListPresenter(
|
||||||
|
isDeveloperModeEnabled: Boolean,
|
||||||
|
isPinFeatureEnabled: Boolean,
|
||||||
|
room: MatrixRoom = FakeMatrixRoom(),
|
||||||
|
): ActionListPresenter {
|
||||||
val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
|
val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
|
||||||
return ActionListPresenter(appPreferencesStore = preferencesStore)
|
val featureFlagsService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(
|
||||||
|
FeatureFlags.PinnedEvents.key to isPinFeatureEnabled,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ActionListPresenter(
|
||||||
|
appPreferencesStore = preferencesStore,
|
||||||
|
featureFlagsService = featureFlagsService,
|
||||||
|
room = room
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ internal fun aMessageEvent(
|
||||||
transactionId: TransactionId? = null,
|
transactionId: TransactionId? = null,
|
||||||
isMine: Boolean = true,
|
isMine: Boolean = true,
|
||||||
isEditable: Boolean = true,
|
isEditable: Boolean = true,
|
||||||
|
canBeRepliedTo: Boolean = true,
|
||||||
content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, formattedBody = null, isEdited = false),
|
content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, formattedBody = null, isEdited = false),
|
||||||
inReplyTo: InReplyToDetails? = null,
|
inReplyTo: InReplyToDetails? = null,
|
||||||
isThreaded: Boolean = false,
|
isThreaded: Boolean = false,
|
||||||
|
|
@ -58,6 +59,7 @@ internal fun aMessageEvent(
|
||||||
sentTime = "",
|
sentTime = "",
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
isEditable = isEditable,
|
isEditable = isEditable,
|
||||||
|
canBeRepliedTo = canBeRepliedTo,
|
||||||
reactionsState = aTimelineItemReactions(count = 0),
|
reactionsState = aTimelineItemReactions(count = 0),
|
||||||
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
|
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
|
||||||
localSendState = sendState,
|
localSendState = sendState,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.pinned.banner
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
|
import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class PinnedMessagesBannerPresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = createPinnedMessagesBannerPresenter(isFeatureEnabled = true)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState).isEqualTo(PinnedMessagesBannerState.Hidden)
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - feature disabled`() = runTest {
|
||||||
|
val presenter = createPinnedMessagesBannerPresenter(isFeatureEnabled = false)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState).isEqualTo(PinnedMessagesBannerState.Hidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - loading state`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
pinnedEventsTimelineResult = { Result.success(FakeTimeline()) }
|
||||||
|
).apply {
|
||||||
|
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
|
||||||
|
}
|
||||||
|
val presenter = createPinnedMessagesBannerPresenter(room = room)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
val loadingState = awaitItem()
|
||||||
|
assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1))
|
||||||
|
assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1)
|
||||||
|
assertThat(loadingState.currentPinnedMessageIndex()).isEqualTo(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - loaded state`() = runTest {
|
||||||
|
val messageContent = aMessageContent("A message")
|
||||||
|
val pinnedEventsTimeline = FakeTimeline(
|
||||||
|
timelineItems = flowOf(
|
||||||
|
listOf(
|
||||||
|
MatrixTimelineItem.Event(
|
||||||
|
uniqueId = "FAKE_UNIQUE_ID",
|
||||||
|
event = anEventTimelineItem(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
content = messageContent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }
|
||||||
|
).apply {
|
||||||
|
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID, AN_EVENT_ID_2)))
|
||||||
|
}
|
||||||
|
val presenter = createPinnedMessagesBannerPresenter(room = room)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
val loadedState = awaitItem() as PinnedMessagesBannerState.Loaded
|
||||||
|
assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(0)
|
||||||
|
assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(1)
|
||||||
|
assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - loaded state - multiple pinned messages`() = runTest {
|
||||||
|
val messageContent1 = aMessageContent("A message")
|
||||||
|
val messageContent2 = aMessageContent("Another message")
|
||||||
|
val pinnedEventsTimeline = FakeTimeline(
|
||||||
|
timelineItems = flowOf(
|
||||||
|
listOf(
|
||||||
|
MatrixTimelineItem.Event(
|
||||||
|
uniqueId = "FAKE_UNIQUE_ID",
|
||||||
|
event = anEventTimelineItem(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
content = messageContent1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MatrixTimelineItem.Event(
|
||||||
|
uniqueId = "FAKE_UNIQUE_ID_2",
|
||||||
|
event = anEventTimelineItem(
|
||||||
|
eventId = AN_EVENT_ID_2,
|
||||||
|
content = messageContent2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }
|
||||||
|
).apply {
|
||||||
|
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID, AN_EVENT_ID_2)))
|
||||||
|
}
|
||||||
|
val presenter = createPinnedMessagesBannerPresenter(room = room)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { loadedState ->
|
||||||
|
loadedState as PinnedMessagesBannerState.Loaded
|
||||||
|
assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(1)
|
||||||
|
assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(2)
|
||||||
|
assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent2.toString())
|
||||||
|
loadedState.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitItem().also { loadedState ->
|
||||||
|
loadedState as PinnedMessagesBannerState.Loaded
|
||||||
|
assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(0)
|
||||||
|
assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(2)
|
||||||
|
assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent1.toString())
|
||||||
|
loadedState.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitItem().also { loadedState ->
|
||||||
|
loadedState as PinnedMessagesBannerState.Loaded
|
||||||
|
assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(1)
|
||||||
|
assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(2)
|
||||||
|
assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent2.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - timeline failed`() = runTest {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
pinnedEventsTimelineResult = { Result.failure(Exception()) }
|
||||||
|
).apply {
|
||||||
|
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
|
||||||
|
}
|
||||||
|
val presenter = createPinnedMessagesBannerPresenter(room = room)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { loadingState ->
|
||||||
|
assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1))
|
||||||
|
assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1)
|
||||||
|
assertThat(loadingState.currentPinnedMessageIndex()).isEqualTo(0)
|
||||||
|
}
|
||||||
|
awaitItem().also { failedState ->
|
||||||
|
assertThat(failedState).isEqualTo(PinnedMessagesBannerState.Hidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createPinnedMessagesBannerPresenter(
|
||||||
|
room: MatrixRoom = FakeMatrixRoom(),
|
||||||
|
itemFactory: PinnedMessagesBannerItemFactory = PinnedMessagesBannerItemFactory(
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
formatter = FakePinnedMessagesBannerFormatter(
|
||||||
|
formatLambda = { event -> "${event.content}" }
|
||||||
|
)
|
||||||
|
),
|
||||||
|
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
|
||||||
|
isFeatureEnabled: Boolean = true,
|
||||||
|
): PinnedMessagesBannerPresenter {
|
||||||
|
return PinnedMessagesBannerPresenter(
|
||||||
|
room = room,
|
||||||
|
itemFactory = itemFactory,
|
||||||
|
isFeatureEnabled = { isFeatureEnabled },
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.pinned.banner
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onRoot
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class PinnedMessagesBannerViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on the banner invoke expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PinnedMessagesBannerEvents>()
|
||||||
|
val state = aLoadedPinnedMessagesBannerState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
)
|
||||||
|
val pinnedEventId = state.currentPinnedMessage.eventId
|
||||||
|
ensureCalledOnceWithParam(pinnedEventId) { callback ->
|
||||||
|
rule.setPinnedMessagesBannerView(
|
||||||
|
state = state,
|
||||||
|
onClick = callback
|
||||||
|
)
|
||||||
|
rule.onRoot().performClick()
|
||||||
|
eventsRecorder.assertSingle(PinnedMessagesBannerEvents.MoveToNextPinned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on view all emit the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PinnedMessagesBannerEvents>(expectEvents = true)
|
||||||
|
val state = aLoadedPinnedMessagesBannerState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setPinnedMessagesBannerView(
|
||||||
|
state = state,
|
||||||
|
onViewAllClick = callback
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.screen_room_pinned_banner_view_all_button_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinnedMessagesBannerView(
|
||||||
|
state: PinnedMessagesBannerState,
|
||||||
|
onClick: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onViewAllClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
PinnedMessagesBannerView(
|
||||||
|
state = state,
|
||||||
|
onClick = onClick,
|
||||||
|
onViewAllClick = onViewAllClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -75,6 +75,7 @@ import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
||||||
|
|
@ -496,6 +497,10 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
|
initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
||||||
|
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO))
|
||||||
|
}
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
||||||
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID))
|
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID))
|
||||||
|
|
@ -541,6 +546,10 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
|
initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
||||||
|
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO))
|
||||||
|
}
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
||||||
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Success(AN_EVENT_ID, 0))
|
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Success(AN_EVENT_ID, 0))
|
||||||
|
|
@ -564,6 +573,10 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
|
initialState.eventSink(TimelineEvents.FocusOnEvent(AN_EVENT_ID))
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
||||||
|
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO))
|
||||||
|
}
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID)
|
||||||
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID))
|
assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID))
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ class TimelineItemGrouperTest {
|
||||||
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
|
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
|
||||||
localSendState = LocalEventSendState.Sent(AN_EVENT_ID),
|
localSendState = LocalEventSendState.Sent(AN_EVENT_ID),
|
||||||
isEditable = false,
|
isEditable = false,
|
||||||
|
canBeRepliedTo = false,
|
||||||
inReplyTo = null,
|
inReplyTo = null,
|
||||||
isThreaded = false,
|
isThreaded = false,
|
||||||
debugInfo = aTimelineItemDebugInfo(),
|
debugInfo = aTimelineItemDebugInfo(),
|
||||||
|
|
|
||||||
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