Merge branch 'release/0.4.2' into main
This commit is contained in:
commit
883f077813
510 changed files with 5596 additions and 1355 deletions
11
.github/workflows/build.yml
vendored
11
.github/workflows/build.yml
vendored
|
|
@ -38,7 +38,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/gradle-build-action@v2.11.1
|
uses: gradle/gradle-build-action@v2.12.0
|
||||||
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
|
||||||
|
|
@ -47,14 +47,15 @@ jobs:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
run: ./gradlew :app:assembleDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :app:assembleGplayDebug :app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload APK APKs
|
- name: Upload APK APKs
|
||||||
if: ${{ matrix.variant == 'debug' }}
|
if: ${{ matrix.variant == 'debug' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: elementx-debug
|
name: elementx-debug
|
||||||
path: |
|
path: |
|
||||||
app/build/outputs/apk/debug/*.apk
|
app/build/outputs/apk/gplay/debug/*.apk
|
||||||
|
app/build/outputs/apk/fdroid/debug/*.apk
|
||||||
- uses: rnkdsh/action-upload-diawi@v1.5.4
|
- uses: rnkdsh/action-upload-diawi@v1.5.4
|
||||||
id: diawi
|
id: diawi
|
||||||
# Do not fail the whole build if Diawi upload fails
|
# Do not fail the whole build if Diawi upload fails
|
||||||
|
|
@ -64,7 +65,7 @@ jobs:
|
||||||
if: ${{ matrix.variant == 'debug' && github.event_name == 'pull_request' && env.token != '' }}
|
if: ${{ matrix.variant == 'debug' && github.event_name == 'pull_request' && env.token != '' }}
|
||||||
with:
|
with:
|
||||||
token: ${{ env.token }}
|
token: ${{ env.token }}
|
||||||
file: app/build/outputs/apk/debug/app-arm64-v8a-debug.apk
|
file: app/build/outputs/apk/gplay/debug/app-gplay-arm64-v8a-debug.apk
|
||||||
- name: Add or update PR comment with QR Code to download APK.
|
- name: Add or update PR comment with QR Code to download APK.
|
||||||
if: ${{ matrix.variant == 'debug' && github.event_name == 'pull_request' && steps.diawi.conclusion == 'success' }}
|
if: ${{ matrix.variant == 'debug' && github.event_name == 'pull_request' && steps.diawi.conclusion == 'success' }}
|
||||||
uses: NejcZdovc/comment-pr@v2
|
uses: NejcZdovc/comment-pr@v2
|
||||||
|
|
@ -82,7 +83,7 @@ jobs:
|
||||||
run: ./gradlew compileReleaseSources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew compileReleaseSources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Compile nightly sources
|
- name: Compile nightly sources
|
||||||
if: ${{ matrix.variant == 'nightly' }}
|
if: ${{ matrix.variant == 'nightly' }}
|
||||||
run: ./gradlew compileNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Compile samples minimal
|
- name: Compile samples minimal
|
||||||
if: ${{ matrix.variant == 'samples' }}
|
if: ${{ matrix.variant == 'samples' }}
|
||||||
run: ./gradlew :samples:minimal:assemble $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :samples:minimal:assemble $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,4 @@ jobs:
|
||||||
# No concurrency required, this is a prerequisite to other actions and should run every time.
|
# No concurrency required, this is a prerequisite to other actions and should run every time.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: gradle/wrapper-validation-action@v1
|
- uses: gradle/wrapper-validation-action@v2
|
||||||
|
|
|
||||||
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
|
|
@ -53,7 +53,7 @@ jobs:
|
||||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||||
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):
|
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):
|
||||||
# app-file should point to an x86 compatible APK file, so upload the x86_64 one (much smaller than the universal APK).
|
# app-file should point to an x86 compatible APK file, so upload the x86_64 one (much smaller than the universal APK).
|
||||||
app-file: app/build/outputs/apk/debug/app-x86_64-debug.apk
|
app-file: app/build/outputs/apk/gplay/debug/app-gplay-x86_64-debug.apk
|
||||||
env: |
|
env: |
|
||||||
USERNAME=maestroelement
|
USERNAME=maestroelement
|
||||||
PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
|
PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
|
||||||
|
|
|
||||||
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
||||||
yes n | towncrier build --version nightly
|
yes n | towncrier build --version nightly
|
||||||
- name: Build and upload Nightly application
|
- name: Build and upload Nightly application
|
||||||
run: |
|
run: |
|
||||||
./gradlew assembleNightly appDistributionUploadNightly $CI_GRADLE_ARG_PROPERTIES
|
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
|
|
@ -45,7 +45,7 @@ jobs:
|
||||||
- name: Additionally upload Nightly APK to browserstack for testing
|
- name: Additionally upload Nightly APK to browserstack for testing
|
||||||
continue-on-error: true # don't block anything by this upload failing (for now)
|
continue-on-error: true # don't block anything by this upload failing (for now)
|
||||||
run: |
|
run: |
|
||||||
curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_PASSWORD" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@app/build/outputs/apk/nightly/app-universal-nightly.apk" -F "custom_id=element-x-android-nightly"
|
curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_PASSWORD" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@app/build/outputs/apk/gplay/nightly/app-gplay-universal-nightly.apk" -F "custom_id=element-x-android-nightly"
|
||||||
env:
|
env:
|
||||||
BROWSERSTACK_USERNAME: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_USERNAME }}
|
BROWSERSTACK_USERNAME: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_USERNAME }}
|
||||||
BROWSERSTACK_PASSWORD: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_ACCESS_KEY }}
|
BROWSERSTACK_PASSWORD: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_ACCESS_KEY }}
|
||||||
|
|
|
||||||
2
.github/workflows/nightlyReports.yml
vendored
2
.github/workflows/nightlyReports.yml
vendored
|
|
@ -62,7 +62,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/gradle-build-action@v2.11.1
|
uses: gradle/gradle-build-action@v2.12.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Dependency analysis
|
- name: Dependency analysis
|
||||||
|
|
|
||||||
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
|
|
@ -40,7 +40,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/gradle-build-action@v2.11.1
|
uses: gradle/gradle-build-action@v2.12.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run code quality check suite
|
- name: Run code quality check suite
|
||||||
|
|
|
||||||
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/gradle-build-action@v2.11.1
|
uses: gradle/gradle-build-action@v2.12.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Record screenshots
|
- name: Record screenshots
|
||||||
|
|
|
||||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
|
|
@ -25,16 +25,16 @@ 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/gradle-build-action@v2.11.1
|
uses: gradle/gradle-build-action@v2.12.0
|
||||||
- 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 }}
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
run: ./gradlew bundleRelease $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload bundle as artifact
|
- name: Upload bundle as artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: elementx-app-bundle-unsigned
|
name: elementx-app-gplay-bundle-unsigned
|
||||||
path: |
|
path: |
|
||||||
app/build/outputs/bundle/release/app-release.aab
|
app/build/outputs/bundle/gplayRelease/app-gplay-release.aab
|
||||||
|
|
|
||||||
2
.github/workflows/sonar.yml
vendored
2
.github/workflows/sonar.yml
vendored
|
|
@ -32,7 +32,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/gradle-build-action@v2.11.1
|
uses: gradle/gradle-build-action@v2.12.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: 🔊 Publish results to Sonar
|
- name: 🔊 Publish results to Sonar
|
||||||
|
|
|
||||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.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/gradle-build-action@v2.11.1
|
uses: gradle/gradle-build-action@v2.12.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
|
||||||
|
|
|
||||||
1
.idea/dictionaries/shared.xml
generated
1
.idea/dictionaries/shared.xml
generated
|
|
@ -16,6 +16,7 @@
|
||||||
<w>snackbar</w>
|
<w>snackbar</w>
|
||||||
<w>swipeable</w>
|
<w>swipeable</w>
|
||||||
<w>textfields</w>
|
<w>textfields</w>
|
||||||
|
<w>tombstoned</w>
|
||||||
</words>
|
</words>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
||||||
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.21" />
|
<option name="version" value="1.9.22" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
34
CHANGES.md
34
CHANGES.md
|
|
@ -1,3 +1,37 @@
|
||||||
|
Changes in Element X v0.4.2 (2024-01-31)
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Matrix SDK 🦀 v0.1.95
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Add 'send private read receipts' option in advanced settings ([#2204](https://github.com/element-hq/element-x-android/issues/2204))
|
||||||
|
- Send typing notification ([#2240](https://github.com/element-hq/element-x-android/issues/2240)). Disabling the sending of typing notification and rendering typing notification will come soon.
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Make the room settings screen update automatically when new room info (name, avatar, topic) is available. ([#921](https://github.com/element-hq/element-x-android/issues/921))
|
||||||
|
- Update timeline items' read receipts when the room members info is loaded. ([#2176](https://github.com/element-hq/element-x-android/issues/2176))
|
||||||
|
- Edited text message bubbles should resize when edited ([#2260](https://github.com/element-hq/element-x-android/issues/2260))
|
||||||
|
- Ensure login and password exclude `\n` ([#2263](https://github.com/element-hq/element-x-android/issues/2263))
|
||||||
|
- Room list Ensure the indicators stay grey if the global setting is set to mention only and a regular message is received. ([#2282](https://github.com/element-hq/element-x-android/issues/2282))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Add a special logging configuration for nightlies so we can get more detailed info for existing issues. ([#+add-special-tracing-configuration-for-nightlies](https://github.com/element-hq/element-x-android/issues/+add-special-tracing-configuration-for-nightlies))
|
||||||
|
- Try mitigating unexpected logouts by making getting/storing session data use a Mutex for synchronization.
|
||||||
|
Also added some more logs so we can understand exactly where it's failing. ([#+try-mitigating-unexpected-logouts](https://github.com/element-hq/element-x-android/issues/+try-mitigating-unexpected-logouts))
|
||||||
|
- Upgrade Material3 Compose to `1.2.0-beta02`.
|
||||||
|
There is also a constraint on a transitive Compose Foundation dependency version (1.6.0-beta02) that fixes the timeline scrolling issue. ([#0-beta02](https://github.com/element-hq/element-x-android/issues/0-beta02))
|
||||||
|
- Disambiguate display name in the timeline. ([#2215](https://github.com/element-hq/element-x-android/issues/2215))
|
||||||
|
- Disambiguate display name in notifications ([#2224](https://github.com/element-hq/element-x-android/issues/2224))
|
||||||
|
- Remove room creation, self-join of room creator and 'this is the beginning of X' timeline items for DMs. ([#2217](https://github.com/element-hq/element-x-android/issues/2217))
|
||||||
|
- Encrypt databases used by the Rust SDK on Nightly and Debug builds. ([#2219](https://github.com/element-hq/element-x-android/issues/2219))
|
||||||
|
- Fallback to UnifiedPush (if available) if the PlayServices are not installed on the device. ([#2248](https://github.com/element-hq/element-x-android/issues/2248))
|
||||||
|
- Add "Report a problem" button to the onboarding screen ([#2275](https://github.com/element-hq/element-x-android/issues/2275))
|
||||||
|
- Add in app logs viewer to the "Report a problem" screen. ([#2276](https://github.com/element-hq/element-x-android/issues/2276))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element X v0.4.1 (2024-01-17)
|
Changes in Element X v0.4.1 (2024-01-17)
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
|
||||||
import extension.allFeaturesImpl
|
import extension.allFeaturesImpl
|
||||||
import extension.allLibrariesImpl
|
import extension.allLibrariesImpl
|
||||||
import extension.allServicesImpl
|
import extension.allServicesImpl
|
||||||
|
import extension.gitBranchName
|
||||||
|
import extension.gitRevision
|
||||||
|
import extension.koverDependencies
|
||||||
|
import extension.setupKover
|
||||||
import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
|
|
@ -36,6 +40,8 @@ plugins {
|
||||||
// id("com.google.gms.google-services")
|
// id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupKover()
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.x"
|
namespace = "io.element.android.x"
|
||||||
|
|
||||||
|
|
@ -50,6 +56,9 @@ android {
|
||||||
abiFilters += listOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
|
abiFilters += listOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildConfigField("String", "GIT_REVISION", "\"${gitRevision()}\"")
|
||||||
|
buildConfigField("String", "GIT_BRANCH_NAME", "\"${gitBranchName()}\"")
|
||||||
|
|
||||||
// Ref: https://developer.android.com/studio/build/configure-apk-splits.html#configure-abi-split
|
// Ref: https://developer.android.com/studio/build/configure-apk-splits.html#configure-abi-split
|
||||||
splits {
|
splits {
|
||||||
// Configures multiple APKs based on ABI.
|
// Configures multiple APKs based on ABI.
|
||||||
|
|
@ -69,7 +78,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
named("debug") {
|
getByName("debug") {
|
||||||
keyAlias = "androiddebugkey"
|
keyAlias = "androiddebugkey"
|
||||||
keyPassword = "android"
|
keyPassword = "android"
|
||||||
storeFile = file("./signature/debug.keystore")
|
storeFile = file("./signature/debug.keystore")
|
||||||
|
|
@ -87,13 +96,13 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
named("debug") {
|
getByName("debug") {
|
||||||
resValue("string", "app_name", "Element X dbg")
|
resValue("string", "app_name", "Element X dbg")
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
|
|
||||||
named("release") {
|
getByName("release") {
|
||||||
resValue("string", "app_name", "Element X")
|
resValue("string", "app_name", "Element X")
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
|
||||||
|
|
@ -124,7 +133,7 @@ android {
|
||||||
// We upload the universal APK to fix this error:
|
// We upload the universal APK to fix this error:
|
||||||
// "App Distribution found more than 1 output file for this variant.
|
// "App Distribution found more than 1 output file for this variant.
|
||||||
// Please contact firebase-support@google.com for help using APK splits with App Distribution."
|
// Please contact firebase-support@google.com for help using APK splits with App Distribution."
|
||||||
artifactPath = "$rootDir/app/build/outputs/apk/nightly/app-universal-nightly.apk"
|
artifactPath = "$rootDir/app/build/outputs/apk/gplay/nightly/app-gplay-universal-nightly.apk"
|
||||||
// artifactType = "AAB"
|
// artifactType = "AAB"
|
||||||
// artifactPath = "$rootDir/app/build/outputs/bundle/nightly/app-nightly.aab"
|
// artifactPath = "$rootDir/app/build/outputs/bundle/nightly/app-nightly.aab"
|
||||||
// This file will be generated by the GitHub action
|
// This file will be generated by the GitHub action
|
||||||
|
|
@ -143,6 +152,20 @@ android {
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
flavorDimensions += "store"
|
||||||
|
productFlavors {
|
||||||
|
create("gplay") {
|
||||||
|
dimension = "store"
|
||||||
|
isDefault = true
|
||||||
|
buildConfigField("String", "SHORT_FLAVOR_DESCRIPTION", "\"G\"")
|
||||||
|
buildConfigField("String", "FLAVOR_DESCRIPTION", "\"GooglePlay\"")
|
||||||
|
}
|
||||||
|
create("fdroid") {
|
||||||
|
dimension = "store"
|
||||||
|
buildConfigField("String", "SHORT_FLAVOR_DESCRIPTION", "\"F\"")
|
||||||
|
buildConfigField("String", "FLAVOR_DESCRIPTION", "\"FDroid\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
|
|
@ -222,6 +245,11 @@ dependencies {
|
||||||
implementation(projects.appconfig)
|
implementation(projects.appconfig)
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
|
|
||||||
|
// Comment to not include firebase in the project
|
||||||
|
"gplayImplementation"(projects.libraries.pushproviders.firebase)
|
||||||
|
// Comment to not include unified push in the project
|
||||||
|
implementation(projects.libraries.pushproviders.unifiedpush)
|
||||||
|
|
||||||
implementation(libs.appyx.core)
|
implementation(libs.appyx.core)
|
||||||
implementation(libs.androidx.splash)
|
implementation(libs.androidx.splash)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
|
|
@ -251,4 +279,5 @@ dependencies {
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
koverDependencies()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ package io.element.android.x.di
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
@ -34,5 +34,5 @@ interface AppBindings {
|
||||||
|
|
||||||
fun lockScreenService(): LockScreenService
|
fun lockScreenService(): LockScreenService
|
||||||
|
|
||||||
fun preferencesStore(): PreferencesStore
|
fun preferencesStore(): AppPreferencesStore
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,16 +85,10 @@ object AppModule {
|
||||||
lowPrivacyLoggingEnabled = false,
|
lowPrivacyLoggingEnabled = false,
|
||||||
versionName = BuildConfig.VERSION_NAME,
|
versionName = BuildConfig.VERSION_NAME,
|
||||||
versionCode = BuildConfig.VERSION_CODE,
|
versionCode = BuildConfig.VERSION_CODE,
|
||||||
// BuildConfig.GIT_REVISION,
|
gitRevision = BuildConfig.GIT_REVISION,
|
||||||
gitRevision = "TODO",
|
gitBranchName = BuildConfig.GIT_BRANCH_NAME,
|
||||||
// BuildConfig.GIT_REVISION_DATE,
|
flavorDescription = BuildConfig.FLAVOR_DESCRIPTION,
|
||||||
gitRevisionDate = "TODO",
|
flavorShortDescription = BuildConfig.SHORT_FLAVOR_DESCRIPTION,
|
||||||
// BuildConfig.GIT_BRANCH_NAME,
|
|
||||||
gitBranchName = "TODO",
|
|
||||||
// BuildConfig.FLAVOR_DESCRIPTION,
|
|
||||||
flavorDescription = "TODO",
|
|
||||||
// BuildConfig.SHORT_FLAVOR_DESCRIPTION,
|
|
||||||
flavorShortDescription = "TODO",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ interface SessionComponent : NodeFactoriesBindings {
|
||||||
interface Builder {
|
interface Builder {
|
||||||
@BindsInstance
|
@BindsInstance
|
||||||
fun client(matrixClient: MatrixClient): Builder
|
fun client(matrixClient: MatrixClient): Builder
|
||||||
|
|
||||||
fun build(): SessionComponent
|
fun build(): SessionComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ fun logApplicationInfo() {
|
||||||
append(BuildConfig.VERSION_CODE)
|
append(BuildConfig.VERSION_CODE)
|
||||||
append(") - ")
|
append(") - ")
|
||||||
append(BuildConfig.BUILD_TYPE)
|
append(BuildConfig.BUILD_TYPE)
|
||||||
|
append(" / ")
|
||||||
|
append(BuildConfig.FLAVOR)
|
||||||
}
|
}
|
||||||
// TODO Get SDK version somehow
|
// TODO Get SDK version somehow
|
||||||
val sdkVersion = "SDK VERSION (TODO)"
|
val sdkVersion = "SDK VERSION (TODO)"
|
||||||
|
|
@ -37,6 +39,7 @@ fun logApplicationInfo() {
|
||||||
Timber.d("----------------------------------------------------------------")
|
Timber.d("----------------------------------------------------------------")
|
||||||
Timber.d("----------------------------------------------------------------")
|
Timber.d("----------------------------------------------------------------")
|
||||||
Timber.d(" Application version: $appVersion")
|
Timber.d(" Application version: $appVersion")
|
||||||
|
Timber.d(" Git SHA: ${BuildConfig.GIT_REVISION}")
|
||||||
Timber.d(" SDK version: $sdkVersion")
|
Timber.d(" SDK version: $sdkVersion")
|
||||||
Timber.d(" Local time: $date")
|
Timber.d(" Local time: $date")
|
||||||
Timber.d("----------------------------------------------------------------")
|
Timber.d("----------------------------------------------------------------")
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,13 @@ class TracingInitializer : Initializer<Unit> {
|
||||||
writesToFilesConfiguration = WriteToFilesConfiguration.Disabled
|
writesToFilesConfiguration = WriteToFilesConfiguration.Disabled
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
val config = if (BuildConfig.BUILD_TYPE == "nightly") {
|
||||||
|
TracingFilterConfigurations.nightly
|
||||||
|
} else {
|
||||||
|
TracingFilterConfigurations.release
|
||||||
|
}
|
||||||
TracingConfiguration(
|
TracingConfiguration(
|
||||||
filterConfiguration = TracingFilterConfigurations.release,
|
filterConfiguration = config,
|
||||||
writesToLogcat = false,
|
writesToLogcat = false,
|
||||||
writesToFilesConfiguration = WriteToFilesConfiguration.Enabled(
|
writesToFilesConfiguration = WriteToFilesConfiguration.Enabled(
|
||||||
directory = bugReporter.logDirectory().absolutePath,
|
directory = bugReporter.logDirectory().absolutePath,
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ dependencies {
|
||||||
implementation(libs.coil)
|
implementation(libs.coil)
|
||||||
|
|
||||||
implementation(projects.features.ftue.api)
|
implementation(projects.features.ftue.api)
|
||||||
|
implementation(projects.features.viewfolder.api)
|
||||||
|
|
||||||
implementation(projects.services.apperror.impl)
|
implementation(projects.services.apperror.impl)
|
||||||
implementation(projects.services.appnavstate.api)
|
implementation(projects.services.appnavstate.api)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
|
|
@ -54,6 +55,10 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
) {
|
) {
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onOpenBugReport()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
|
|
@ -91,6 +96,10 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
||||||
override fun onOpenDeveloperSettings() {
|
override fun onOpenDeveloperSettings() {
|
||||||
backstack.push(NavTarget.ConfigureTracing)
|
backstack.push(NavTarget.ConfigureTracing)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onReportProblem() {
|
||||||
|
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onBoardingEntryPoint
|
onBoardingEntryPoint
|
||||||
.nodeBuilder(this, buildContext)
|
.nodeBuilder(this, buildContext)
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import io.element.android.features.login.api.oidc.OidcAction
|
||||||
import io.element.android.features.login.api.oidc.OidcActionFlow
|
import io.element.android.features.login.api.oidc.OidcActionFlow
|
||||||
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
||||||
import io.element.android.features.signedout.api.SignedOutEntryPoint
|
import io.element.android.features.signedout.api.SignedOutEntryPoint
|
||||||
|
import io.element.android.features.viewfolder.api.ViewFolderEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
|
@ -70,6 +71,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
private val matrixClientsHolder: MatrixClientsHolder,
|
private val matrixClientsHolder: MatrixClientsHolder,
|
||||||
private val presenter: RootPresenter,
|
private val presenter: RootPresenter,
|
||||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||||
|
private val viewFolderEntryPoint: ViewFolderEntryPoint,
|
||||||
private val signedOutEntryPoint: SignedOutEntryPoint,
|
private val signedOutEntryPoint: SignedOutEntryPoint,
|
||||||
private val intentResolver: IntentResolver,
|
private val intentResolver: IntentResolver,
|
||||||
private val oidcActionFlow: OidcActionFlow,
|
private val oidcActionFlow: OidcActionFlow,
|
||||||
|
|
@ -141,7 +143,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
onSuccess(sessionId)
|
onSuccess(sessionId)
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
Timber.v("Failed to restore session $sessionId")
|
Timber.e(it, "Failed to restore session $sessionId")
|
||||||
onFailure()
|
onFailure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -194,6 +196,11 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object BugReport : NavTarget
|
data object BugReport : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ViewLogs(
|
||||||
|
val rootPath: String,
|
||||||
|
) : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -210,7 +217,14 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
||||||
}
|
}
|
||||||
NavTarget.NotLoggedInFlow -> createNode<NotLoggedInFlowNode>(buildContext)
|
NavTarget.NotLoggedInFlow -> {
|
||||||
|
val callback = object : NotLoggedInFlowNode.Callback {
|
||||||
|
override fun onOpenBugReport() {
|
||||||
|
backstack.push(NavTarget.BugReport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<NotLoggedInFlowNode>(buildContext, plugins = listOf(callback))
|
||||||
|
}
|
||||||
is NavTarget.SignedOutFlow -> {
|
is NavTarget.SignedOutFlow -> {
|
||||||
signedOutEntryPoint.nodeBuilder(this, buildContext)
|
signedOutEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.params(
|
.params(
|
||||||
|
|
@ -226,12 +240,31 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
override fun onBugReportSent() {
|
override fun onBugReportSent() {
|
||||||
backstack.pop()
|
backstack.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onViewLogs(basePath: String) {
|
||||||
|
backstack.push(NavTarget.ViewLogs(rootPath = basePath))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bugReportEntryPoint
|
bugReportEntryPoint
|
||||||
.nodeBuilder(this, buildContext)
|
.nodeBuilder(this, buildContext)
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
is NavTarget.ViewLogs -> {
|
||||||
|
val callback = object : ViewFolderEntryPoint.Callback {
|
||||||
|
override fun onDone() {
|
||||||
|
backstack.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val params = ViewFolderEntryPoint.Params(
|
||||||
|
rootPath = navTarget.rootPath,
|
||||||
|
)
|
||||||
|
viewFolderEntryPoint
|
||||||
|
.nodeBuilder(this, buildContext)
|
||||||
|
.params(params)
|
||||||
|
.callback(callback)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -118,14 +118,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchRoomMembers() = lifecycleScope.launch {
|
private fun fetchRoomMembers() = lifecycleScope.launch {
|
||||||
val room = inputs.room
|
inputs.room.updateMembers()
|
||||||
room.updateMembers()
|
|
||||||
.onFailure {
|
|
||||||
Timber.e(it, "Fail to fetch members for room ${room.roomId}")
|
|
||||||
}
|
|
||||||
.onSuccess {
|
|
||||||
Timber.v("Success fetching members for room ${room.roomId}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRoomDetailsNode(buildContext: BuildContext, initialTarget: RoomDetailsEntryPoint.InitialTarget): Node {
|
private fun createRoomDetailsNode(buildContext: BuildContext, initialTarget: RoomDetailsEntryPoint.InitialTarget): Node {
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,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.3.9")
|
detektPlugins("io.nlopez.compose.rules:detekt:0.3.11")
|
||||||
}
|
}
|
||||||
|
|
||||||
// KtLint
|
// KtLint
|
||||||
|
|
|
||||||
2
fastlane/metadata/android/en-US/changelogs/40004020.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40004020.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: be able to send a problem from the first screen, and add an internal log viewer. Be able to send private read receipt, send typing notification, improve performance.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
|
|
@ -45,7 +45,7 @@ import io.element.android.features.call.CallForegroundService
|
||||||
import io.element.android.features.call.CallType
|
import io.element.android.features.call.CallType
|
||||||
import io.element.android.features.call.di.CallBindings
|
import io.element.android.features.call.di.CallBindings
|
||||||
import io.element.android.features.call.utils.CallIntentDataParser
|
import io.element.android.features.call.utils.CallIntentDataParser
|
||||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
|
||||||
|
|
||||||
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
||||||
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
||||||
@Inject lateinit var preferencesStore: PreferencesStore
|
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
||||||
|
|
||||||
private lateinit var presenter: CallScreenPresenter
|
private lateinit var presenter: CallScreenPresenter
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val theme by remember {
|
val theme by remember {
|
||||||
preferencesStore.getThemeFlow().mapToTheme()
|
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||||
}
|
}
|
||||||
.collectAsState(initial = Theme.System)
|
.collectAsState(initial = Theme.System)
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.call.utils
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.appconfig.ElementCallConfig
|
import io.element.android.appconfig.ElementCallConfig
|
||||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -31,7 +31,7 @@ import javax.inject.Inject
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultCallWidgetProvider @Inject constructor(
|
class DefaultCallWidgetProvider @Inject constructor(
|
||||||
private val matrixClientsProvider: MatrixClientProvider,
|
private val matrixClientsProvider: MatrixClientProvider,
|
||||||
private val preferencesStore: PreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
||||||
) : CallWidgetProvider {
|
) : CallWidgetProvider {
|
||||||
override suspend fun getWidget(
|
override suspend fun getWidget(
|
||||||
|
|
@ -42,7 +42,7 @@ class DefaultCallWidgetProvider @Inject constructor(
|
||||||
theme: String?,
|
theme: String?,
|
||||||
): Result<Pair<MatrixWidgetDriver, String>> = runCatching {
|
): Result<Pair<MatrixWidgetDriver, String>> = runCatching {
|
||||||
val room = matrixClientsProvider.getOrRestore(sessionId).getOrThrow().getRoom(roomId) ?: error("Room not found")
|
val room = matrixClientsProvider.getOrRestore(sessionId).getOrThrow().getRoom(roomId) ?: error("Room not found")
|
||||||
val baseUrl = preferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() ?: ElementCallConfig.DEFAULT_BASE_URL
|
val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() ?: ElementCallConfig.DEFAULT_BASE_URL
|
||||||
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl)
|
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl)
|
||||||
val callUrl = room.generateWidgetWebViewUrl(widgetSettings, clientId, languageTag, theme).getOrThrow()
|
val callUrl = room.generateWidgetWebViewUrl(widgetSettings, clientId, languageTag, theme).getOrThrow()
|
||||||
room.getWidgetDriver(widgetSettings).getOrThrow() to callUrl
|
room.getWidgetDriver(widgetSettings).getOrThrow() to callUrl
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
package io.element.android.features.call.utils
|
package io.element.android.features.call.utils
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
|
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
|
@ -94,14 +94,14 @@ class DefaultCallWidgetProviderTest {
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
val preferencesStore = InMemoryPreferencesStore().apply {
|
val preferencesStore = InMemoryAppPreferencesStore().apply {
|
||||||
setCustomElementCallBaseUrl("https://custom.element.io")
|
setCustomElementCallBaseUrl("https://custom.element.io")
|
||||||
}
|
}
|
||||||
val settingsProvider = FakeCallWidgetSettingsProvider()
|
val settingsProvider = FakeCallWidgetSettingsProvider()
|
||||||
val provider = createProvider(
|
val provider = createProvider(
|
||||||
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
|
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
|
||||||
callWidgetSettingsProvider = settingsProvider,
|
callWidgetSettingsProvider = settingsProvider,
|
||||||
preferencesStore = preferencesStore,
|
appPreferencesStore = preferencesStore,
|
||||||
)
|
)
|
||||||
provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme")
|
provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme")
|
||||||
|
|
||||||
|
|
@ -110,11 +110,11 @@ class DefaultCallWidgetProviderTest {
|
||||||
|
|
||||||
private fun createProvider(
|
private fun createProvider(
|
||||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
preferencesStore: PreferencesStore = InMemoryPreferencesStore(),
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider()
|
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider()
|
||||||
) = DefaultCallWidgetProvider(
|
) = DefaultCallWidgetProvider(
|
||||||
matrixClientProvider,
|
matrixClientProvider,
|
||||||
preferencesStore,
|
appPreferencesStore,
|
||||||
callWidgetSettingsProvider,
|
callWidgetSettingsProvider,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,11 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow
|
import io.element.android.libraries.matrix.ui.components.CheckableUserRow
|
||||||
import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
|
import io.element.android.libraries.matrix.ui.components.CheckableUserRowData
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
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.model.getBestName
|
||||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -36,23 +37,24 @@ fun SearchMultipleUsersResultItem(
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (searchResult.isUnresolved) {
|
val data = if (searchResult.isUnresolved) {
|
||||||
CheckableUnresolvedUserRow(
|
CheckableUserRowData.Unresolved(
|
||||||
checked = isUserSelected,
|
|
||||||
modifier = modifier,
|
|
||||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||||
id = searchResult.matrixUser.userId.value,
|
id = searchResult.matrixUser.userId.value,
|
||||||
onCheckedChange = onCheckedChange,
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
CheckableMatrixUserRow(
|
CheckableUserRowData.Resolved(
|
||||||
checked = isUserSelected,
|
name = searchResult.matrixUser.getBestName(),
|
||||||
modifier = modifier,
|
subtext = if (searchResult.matrixUser.displayName.isNullOrEmpty()) null else searchResult.matrixUser.userId.value,
|
||||||
matrixUser = searchResult.matrixUser,
|
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||||
avatarSize = AvatarSize.UserListItem,
|
|
||||||
onCheckedChange = onCheckedChange,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
CheckableUserRow(
|
||||||
|
checked = isUserSelected,
|
||||||
|
modifier = modifier,
|
||||||
|
data = data,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.ftue.impl.migration
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import io.element.android.features.ftue.impl.R
|
import io.element.android.features.ftue.impl.R
|
||||||
|
|
@ -32,8 +34,9 @@ fun MigrationScreenView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (migrationState.isMigrating.not()) {
|
if (migrationState.isMigrating.not()) {
|
||||||
|
val latestOnMigrationFinished by rememberUpdatedState(onMigrationFinished)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
onMigrationFinished()
|
latestOnMigrationFinished()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SunsetPage(
|
SunsetPage(
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ class InviteListPresenter @Inject constructor(
|
||||||
AvatarData(
|
AvatarData(
|
||||||
id = roomId.value,
|
id = roomId.value,
|
||||||
name = name,
|
name = name,
|
||||||
url = avatarURLString,
|
url = avatarUrl,
|
||||||
size = AvatarSize.RoomInviteItem,
|
size = AvatarSize.RoomInviteItem,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
|
@ -57,8 +59,9 @@ fun InviteListView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (state.acceptedAction is AsyncData.Success) {
|
if (state.acceptedAction is AsyncData.Success) {
|
||||||
|
val latestOnInviteAccepted by rememberUpdatedState(onInviteAccepted)
|
||||||
LaunchedEffect(state.acceptedAction) {
|
LaunchedEffect(state.acceptedAction) {
|
||||||
onInviteAccepted(state.acceptedAction.data)
|
latestOnInviteAccepted(state.acceptedAction.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
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_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
|
|
@ -39,6 +38,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
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.room.aRoomSummaryDetails
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||||
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
||||||
|
|
@ -425,14 +425,12 @@ class InviteListPresenterTests {
|
||||||
postInviteRooms(
|
postInviteRooms(
|
||||||
listOf(
|
listOf(
|
||||||
RoomSummary.Filled(
|
RoomSummary.Filled(
|
||||||
RoomSummaryDetails(
|
aRoomSummaryDetails(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
name = A_ROOM_NAME,
|
name = A_ROOM_NAME,
|
||||||
avatarURLString = null,
|
avatarUrl = null,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
lastMessage = null,
|
lastMessage = null,
|
||||||
lastMessageTimestamp = null,
|
|
||||||
unreadNotificationCount = 0,
|
|
||||||
inviter = RoomMember(
|
inviter = RoomMember(
|
||||||
userId = A_USER_ID,
|
userId = A_USER_ID,
|
||||||
displayName = A_USER_NAME,
|
displayName = A_USER_NAME,
|
||||||
|
|
@ -454,14 +452,12 @@ class InviteListPresenterTests {
|
||||||
postInviteRooms(
|
postInviteRooms(
|
||||||
listOf(
|
listOf(
|
||||||
RoomSummary.Filled(
|
RoomSummary.Filled(
|
||||||
RoomSummaryDetails(
|
aRoomSummaryDetails(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
name = A_ROOM_NAME,
|
name = A_ROOM_NAME,
|
||||||
avatarURLString = null,
|
avatarUrl = null,
|
||||||
isDirect = true,
|
isDirect = true,
|
||||||
lastMessage = null,
|
lastMessage = null,
|
||||||
lastMessageTimestamp = null,
|
|
||||||
unreadNotificationCount = 0,
|
|
||||||
inviter = RoomMember(
|
inviter = RoomMember(
|
||||||
userId = A_USER_ID,
|
userId = A_USER_ID,
|
||||||
displayName = A_USER_NAME,
|
displayName = A_USER_NAME,
|
||||||
|
|
@ -480,14 +476,12 @@ class InviteListPresenterTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aRoomSummary(id: RoomId = A_ROOM_ID) = RoomSummary.Filled(
|
private fun aRoomSummary(id: RoomId = A_ROOM_ID) = RoomSummary.Filled(
|
||||||
RoomSummaryDetails(
|
aRoomSummaryDetails(
|
||||||
roomId = id,
|
roomId = id,
|
||||||
name = A_ROOM_NAME,
|
name = A_ROOM_NAME,
|
||||||
avatarURLString = null,
|
avatarUrl = null,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
lastMessage = null,
|
lastMessage = null,
|
||||||
lastMessageTimestamp = null,
|
|
||||||
unreadNotificationCount = 0,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +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="leave_conversation_alert_subtitle">"Opravdu chcete opustit tuto konverzaci? Tato konverzace není veřejná a bez pozvánky se k ní nebudete moci znovu připojit."</string>
|
||||||
<string name="leave_room_alert_empty_subtitle">"Opravdu chcete opustit tuto místnost? Jste tu jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás."</string>
|
<string name="leave_room_alert_empty_subtitle">"Opravdu chcete opustit tuto místnost? Jste tu jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás."</string>
|
||||||
<string name="leave_room_alert_private_subtitle">"Opravdu chcete opustit tuto místnost? Tato místnost není veřejná a bez pozvánky se nebudete moci znovu připojit."</string>
|
<string name="leave_room_alert_private_subtitle">"Opravdu chcete opustit tuto místnost? Tato místnost není veřejná a bez pozvánky se nebudete moci znovu připojit."</string>
|
||||||
<string name="leave_room_alert_subtitle">"Opravdu chcete opustit místnost?"</string>
|
<string name="leave_room_alert_subtitle">"Opravdu chcete opustit místnost?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +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="leave_conversation_alert_subtitle">"Êtes-vous sûr de vouloir quitter cette discussion? Vous ne pourrez pas la rejoindre à nouveau sans y être invité."</string>
|
||||||
<string name="leave_room_alert_empty_subtitle">"Êtes-vous sûr de vouloir quitter ce salon ? Vous êtes la seule personne ici. Si vous partez, personne ne pourra rejoindre le salon à l’avenir, y compris vous."</string>
|
<string name="leave_room_alert_empty_subtitle">"Êtes-vous sûr de vouloir quitter ce salon ? Vous êtes la seule personne ici. Si vous partez, personne ne pourra rejoindre le salon à l’avenir, y compris vous."</string>
|
||||||
<string name="leave_room_alert_private_subtitle">"Êtes-vous sûr de vouloir quitter ce salon ? Ce salon n’est pas public et vous ne pourrez pas le rejoindre sans invitation."</string>
|
<string name="leave_room_alert_private_subtitle">"Êtes-vous sûr de vouloir quitter ce salon ? Ce salon n’est pas public et vous ne pourrez pas le rejoindre sans invitation."</string>
|
||||||
<string name="leave_room_alert_subtitle">"Êtes-vous sûr de vouloir quitter le salon ?"</string>
|
<string name="leave_room_alert_subtitle">"Êtes-vous sûr de vouloir quitter le salon ?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +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="leave_conversation_alert_subtitle">"Vuoi davvero abbandonare questa conversazione? La conversazione non è pubblica e non potrai rientrare senza un invito."</string>
|
||||||
<string name="leave_room_alert_empty_subtitle">"Sei sicuro di voler lasciare questa stanza? Sei l\'unica persona presente. Se esci, nessuno potrà unirsi in futuro, te compreso."</string>
|
<string name="leave_room_alert_empty_subtitle">"Sei sicuro di voler lasciare questa stanza? Sei l\'unica persona presente. Se esci, nessuno potrà unirsi in futuro, te compreso."</string>
|
||||||
<string name="leave_room_alert_private_subtitle">"Sei sicuro di voler lasciare questa stanza? Questa stanza non è pubblica e non potrai rientrare senza un invito."</string>
|
<string name="leave_room_alert_private_subtitle">"Sei sicuro di voler lasciare questa stanza? Questa stanza non è pubblica e non potrai rientrare senza un invito."</string>
|
||||||
<string name="leave_room_alert_subtitle">"Sei sicuro di voler lasciare la stanza?"</string>
|
<string name="leave_room_alert_subtitle">"Sei sicuro di voler lasciare la stanza?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +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="leave_conversation_alert_subtitle">"Вы уверены, что хотите покинуть беседу?"</string>
|
||||||
<string name="leave_room_alert_empty_subtitle">"Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас."</string>
|
<string name="leave_room_alert_empty_subtitle">"Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас."</string>
|
||||||
<string name="leave_room_alert_private_subtitle">"Вы уверены, что хотите покинуть эту комнату? Эта комната не является публичной, и Вы не сможете присоединиться к ней без приглашения."</string>
|
<string name="leave_room_alert_private_subtitle">"Вы уверены, что хотите покинуть эту комнату? Эта комната не является публичной, и Вы не сможете присоединиться к ней без приглашения."</string>
|
||||||
<string name="leave_room_alert_subtitle">"Вы уверены, что хотите покинуть комнату?"</string>
|
<string name="leave_room_alert_subtitle">"Вы уверены, что хотите покинуть комнату?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +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="leave_conversation_alert_subtitle">"Ste si istí, že chcete opustiť konverzáciu?"</string>
|
||||||
<string name="leave_room_alert_empty_subtitle">"Ste si istí, že chcete opustiť túto miestnosť? Ste tu jediná osoba. Ak odídete, nikto sa do nej nebude môcť v budúcnosti pripojiť, vrátane vás."</string>
|
<string name="leave_room_alert_empty_subtitle">"Ste si istí, že chcete opustiť túto miestnosť? Ste tu jediná osoba. Ak odídete, nikto sa do nej nebude môcť v budúcnosti pripojiť, vrátane vás."</string>
|
||||||
<string name="leave_room_alert_private_subtitle">"Ste si istí, že chcete opustiť túto miestnosť? Táto miestnosť nie je verejná a bez pozvania sa do nej nebudete môcť vrátiť."</string>
|
<string name="leave_room_alert_private_subtitle">"Ste si istí, že chcete opustiť túto miestnosť? Táto miestnosť nie je verejná a bez pozvania sa do nej nebudete môcť vrátiť."</string>
|
||||||
<string name="leave_room_alert_subtitle">"Ste si istí, že chcete opustiť miestnosť?"</string>
|
<string name="leave_room_alert_subtitle">"Ste si istí, že chcete opustiť miestnosť?"</string>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.lockscreen.impl.unlock
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
|
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
|
||||||
import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback
|
import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback
|
||||||
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
|
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
|
||||||
|
|
@ -30,15 +32,16 @@ class PinUnlockHelper @Inject constructor(
|
||||||
) {
|
) {
|
||||||
@Composable
|
@Composable
|
||||||
fun OnUnlockEffect(onUnlock: () -> Unit) {
|
fun OnUnlockEffect(onUnlock: () -> Unit) {
|
||||||
|
val latestOnUnlock by rememberUpdatedState(onUnlock)
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() {
|
val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() {
|
||||||
override fun onBiometricUnlockSuccess() {
|
override fun onBiometricUnlockSuccess() {
|
||||||
onUnlock()
|
latestOnUnlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val pinCodeVerifiedCallback = object : DefaultPinCodeManagerCallback() {
|
val pinCodeVerifiedCallback = object : DefaultPinCodeManagerCallback() {
|
||||||
override fun onPinCodeVerified() {
|
override fun onPinCodeVerified() {
|
||||||
onUnlock()
|
latestOnUnlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
biometricUnlockManager.addCallback(biometricUnlockCallback)
|
biometricUnlockManager.addCallback(biometricUnlockCallback)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.login.impl.changeserver
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
|
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
|
||||||
|
|
@ -63,8 +65,11 @@ fun ChangeServerView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is AsyncData.Loading -> ProgressDialog()
|
is AsyncData.Loading -> ProgressDialog()
|
||||||
is AsyncData.Success -> LaunchedEffect(state.changeServerAction) {
|
is AsyncData.Success -> {
|
||||||
onDone()
|
val latestOnDone by rememberUpdatedState(onDone)
|
||||||
|
LaunchedEffect(state.changeServerAction) {
|
||||||
|
latestOnDone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AsyncData.Uninitialized -> Unit
|
AsyncData.Uninitialized -> Unit
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -194,15 +194,17 @@ private fun LoginForm(
|
||||||
.onTabOrEnterKeyFocusNext(focusManager)
|
.onTabOrEnterKeyFocusNext(focusManager)
|
||||||
.testTag(TestTags.loginEmailUsername)
|
.testTag(TestTags.loginEmailUsername)
|
||||||
.autofill(autofillTypes = listOf(AutofillType.Username), onFill = {
|
.autofill(autofillTypes = listOf(AutofillType.Username), onFill = {
|
||||||
loginFieldState = it
|
val sanitized = it.sanitize()
|
||||||
eventSink(LoginPasswordEvents.SetLogin(it))
|
loginFieldState = sanitized
|
||||||
|
eventSink(LoginPasswordEvents.SetLogin(sanitized))
|
||||||
}),
|
}),
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(text = stringResource(CommonStrings.common_username))
|
Text(text = stringResource(CommonStrings.common_username))
|
||||||
},
|
},
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
loginFieldState = it
|
val sanitized = it.sanitize()
|
||||||
eventSink(LoginPasswordEvents.SetLogin(it))
|
loginFieldState = sanitized
|
||||||
|
eventSink(LoginPasswordEvents.SetLogin(sanitized))
|
||||||
},
|
},
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
keyboardType = KeyboardType.Email,
|
keyboardType = KeyboardType.Email,
|
||||||
|
|
@ -224,7 +226,6 @@ private fun LoginForm(
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
if (state.loginAction is AsyncData.Loading) {
|
if (state.loginAction is AsyncData.Loading) {
|
||||||
// Ensure password is hidden when user submits the form
|
// Ensure password is hidden when user submits the form
|
||||||
|
|
@ -239,12 +240,14 @@ private fun LoginForm(
|
||||||
.onTabOrEnterKeyFocusNext(focusManager)
|
.onTabOrEnterKeyFocusNext(focusManager)
|
||||||
.testTag(TestTags.loginPassword)
|
.testTag(TestTags.loginPassword)
|
||||||
.autofill(autofillTypes = listOf(AutofillType.Password), onFill = {
|
.autofill(autofillTypes = listOf(AutofillType.Password), onFill = {
|
||||||
passwordFieldState = it
|
val sanitized = it.sanitize()
|
||||||
eventSink(LoginPasswordEvents.SetPassword(it))
|
passwordFieldState = sanitized
|
||||||
|
eventSink(LoginPasswordEvents.SetPassword(sanitized))
|
||||||
}),
|
}),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
passwordFieldState = it
|
val sanitized = it.sanitize()
|
||||||
eventSink(LoginPasswordEvents.SetPassword(it))
|
passwordFieldState = sanitized
|
||||||
|
eventSink(LoginPasswordEvents.SetPassword(sanitized))
|
||||||
},
|
},
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(text = stringResource(CommonStrings.common_password))
|
Text(text = stringResource(CommonStrings.common_password))
|
||||||
|
|
@ -272,6 +275,13 @@ private fun LoginForm(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that the string does not contain any new line characters, which can happen when pasting values.
|
||||||
|
*/
|
||||||
|
private fun String.sanitize(): String {
|
||||||
|
return replace("\n", "")
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
|
private fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.logout.impl.ui
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import io.element.android.features.logout.impl.R
|
import io.element.android.features.logout.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
@ -52,9 +54,11 @@ fun LogoutActionDialog(
|
||||||
onRetry = onForceLogoutClicked,
|
onRetry = onForceLogoutClicked,
|
||||||
onDismiss = onDismissError,
|
onDismiss = onDismissError,
|
||||||
)
|
)
|
||||||
is AsyncAction.Success ->
|
is AsyncAction.Success -> {
|
||||||
|
val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout)
|
||||||
LaunchedEffect(state) {
|
LaunchedEffect(state) {
|
||||||
onSuccessLogout(state.data)
|
latestOnSuccessLogout(state.data)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ import io.element.android.features.messages.impl.utils.messagesummary.MessageSum
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||||
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
|
||||||
|
|
@ -81,10 +81,10 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
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.user.CurrentSessionIdHolder
|
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||||
import io.element.android.libraries.matrix.ui.room.canRedactAsState
|
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.matrix.ui.room.canSendMessageAsState
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -107,12 +107,11 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
private val messageSummaryFormatter: MessageSummaryFormatter,
|
private val messageSummaryFormatter: MessageSummaryFormatter,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val clipboardHelper: ClipboardHelper,
|
private val clipboardHelper: ClipboardHelper,
|
||||||
private val preferencesStore: PreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val featureFlagsService: FeatureFlagService,
|
private val featureFlagsService: FeatureFlagService,
|
||||||
private val htmlConverterProvider: HtmlConverterProvider,
|
private val htmlConverterProvider: HtmlConverterProvider,
|
||||||
@Assisted private val navigator: MessagesNavigator,
|
@Assisted private val navigator: MessagesNavigator,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val currentSessionIdHolder: CurrentSessionIdHolder,
|
|
||||||
) : Presenter<MessagesState> {
|
) : Presenter<MessagesState> {
|
||||||
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
|
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
|
||||||
|
|
||||||
|
|
@ -123,7 +122,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): MessagesState {
|
override fun present(): MessagesState {
|
||||||
htmlConverterProvider.Update(currentUserId = currentSessionIdHolder.current)
|
htmlConverterProvider.Update(currentUserId = room.sessionId)
|
||||||
|
|
||||||
val roomInfo by room.roomInfoFlow.collectAsState(null)
|
val roomInfo by room.roomInfoFlow.collectAsState(null)
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
|
@ -138,7 +137,8 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
||||||
val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value)
|
val userHasPermissionToRedactOwn by room.canRedactOwnAsState(updateKey = syncUpdateFlow.value)
|
||||||
|
val userHasPermissionToRedactOther by room.canRedactOtherAsState(updateKey = syncUpdateFlow.value)
|
||||||
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION_SENT, updateKey = syncUpdateFlow.value)
|
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION_SENT, 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 }
|
||||||
|
|
@ -155,15 +155,15 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(currentSessionIdHolder.current) {
|
LaunchedEffect(syncUpdateFlow.value) {
|
||||||
withContext(dispatchers.io) {
|
withContext(dispatchers.io) {
|
||||||
canJoinCall = room.canUserJoinCall(userId = currentSessionIdHolder.current).getOrDefault(false)
|
canJoinCall = room.canUserJoinCall(room.sessionId).getOrDefault(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) }
|
val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) }
|
||||||
var showReinvitePrompt by remember { mutableStateOf(false) }
|
var showReinvitePrompt by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) {
|
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow.value) {
|
||||||
withContext(dispatchers.io) {
|
withContext(dispatchers.io) {
|
||||||
showReinvitePrompt = !hasDismissedInviteDialog && composerState.hasFocus && room.isDirect && room.activeMemberCount == 1L
|
showReinvitePrompt = !hasDismissedInviteDialog && composerState.hasFocus && room.isDirect && room.activeMemberCount == 1L
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +176,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
|
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
|
val enableTextFormatting by appPreferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
|
||||||
|
|
||||||
var enableVoiceMessages by remember { mutableStateOf(false) }
|
var enableVoiceMessages by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(featureFlagsService) {
|
LaunchedEffect(featureFlagsService) {
|
||||||
|
|
@ -219,7 +219,8 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
roomAvatar = roomAvatar,
|
roomAvatar = roomAvatar,
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||||
userHasPermissionToRedact = userHasPermissionToRedact,
|
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
|
||||||
|
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
|
||||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
||||||
composerState = composerState,
|
composerState = composerState,
|
||||||
voiceMessageComposerState = voiceMessageComposerState,
|
voiceMessageComposerState = voiceMessageComposerState,
|
||||||
|
|
@ -312,7 +313,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleActionEdit(
|
private fun handleActionEdit(
|
||||||
targetEvent: TimelineItem.Event,
|
targetEvent: TimelineItem.Event,
|
||||||
composerState: MessageComposerState,
|
composerState: MessageComposerState,
|
||||||
enableTextFormatting: Boolean,
|
enableTextFormatting: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ data class MessagesState(
|
||||||
val roomName: AsyncData<String>,
|
val roomName: AsyncData<String>,
|
||||||
val roomAvatar: AsyncData<AvatarData>,
|
val roomAvatar: AsyncData<AvatarData>,
|
||||||
val userHasPermissionToSendMessage: Boolean,
|
val userHasPermissionToSendMessage: Boolean,
|
||||||
val userHasPermissionToRedact: Boolean,
|
val userHasPermissionToRedactOwn: Boolean,
|
||||||
|
val userHasPermissionToRedactOther: Boolean,
|
||||||
val userHasPermissionToSendReaction: Boolean,
|
val userHasPermissionToSendReaction: Boolean,
|
||||||
val composerState: MessageComposerState,
|
val composerState: MessageComposerState,
|
||||||
val voiceMessageComposerState: VoiceMessageComposerState,
|
val voiceMessageComposerState: VoiceMessageComposerState,
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,8 @@ fun aMessagesState() = MessagesState(
|
||||||
roomName = AsyncData.Success("Room name"),
|
roomName = AsyncData.Success("Room name"),
|
||||||
roomAvatar = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
|
roomAvatar = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
|
||||||
userHasPermissionToSendMessage = true,
|
userHasPermissionToSendMessage = true,
|
||||||
userHasPermissionToRedact = false,
|
userHasPermissionToRedactOwn = false,
|
||||||
|
userHasPermissionToRedactOther = false,
|
||||||
userHasPermissionToSendReaction = true,
|
userHasPermissionToSendReaction = true,
|
||||||
composerState = aMessageComposerState().copy(
|
composerState = aMessageComposerState().copy(
|
||||||
richTextEditorState = RichTextEditorState("Hello", initialFocus = true),
|
richTextEditorState = RichTextEditorState("Hello", initialFocus = true),
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,10 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
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.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
|
@ -160,7 +162,8 @@ fun MessagesView(
|
||||||
state.actionListState.eventSink(
|
state.actionListState.eventSink(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
event = event,
|
event = event,
|
||||||
canRedact = state.userHasPermissionToRedact,
|
canRedactOwn = state.userHasPermissionToRedactOwn,
|
||||||
|
canRedactOther = state.userHasPermissionToRedactOther,
|
||||||
canSendMessage = state.userHasPermissionToSendMessage,
|
canSendMessage = state.userHasPermissionToSendMessage,
|
||||||
canSendReaction = state.userHasPermissionToSendReaction,
|
canSendReaction = state.userHasPermissionToSendReaction,
|
||||||
)
|
)
|
||||||
|
|
@ -293,8 +296,11 @@ private fun AttachmentStateView(
|
||||||
) {
|
) {
|
||||||
when (state) {
|
when (state) {
|
||||||
AttachmentsState.None -> Unit
|
AttachmentsState.None -> Unit
|
||||||
is AttachmentsState.Previewing -> LaunchedEffect(state) {
|
is AttachmentsState.Previewing -> {
|
||||||
onPreviewAttachments(state.attachments)
|
val latestOnPreviewAttachments by rememberUpdatedState(onPreviewAttachments)
|
||||||
|
LaunchedEffect(state) {
|
||||||
|
latestOnPreviewAttachments(state.attachments)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is AttachmentsState.Sending -> {
|
is AttachmentsState.Sending -> {
|
||||||
ProgressDialog(
|
ProgressDialog(
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ 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 canRedact: Boolean,
|
val canRedactOwn: Boolean,
|
||||||
|
val canRedactOther: Boolean,
|
||||||
val canSendMessage: Boolean,
|
val canSendMessage: Boolean,
|
||||||
val canSendReaction: Boolean,
|
val canSendReaction: Boolean,
|
||||||
) : ActionListEvents
|
) : ActionListEvents
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
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.canReact
|
import io.element.android.features.messages.impl.timeline.model.event.canReact
|
||||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -39,7 +39,7 @@ import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ActionListPresenter @Inject constructor(
|
class ActionListPresenter @Inject constructor(
|
||||||
private val preferencesStore: PreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
) : Presenter<ActionListState> {
|
) : Presenter<ActionListState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ActionListState {
|
override fun present(): ActionListState {
|
||||||
|
|
@ -49,14 +49,15 @@ class ActionListPresenter @Inject constructor(
|
||||||
mutableStateOf(ActionListState.Target.None)
|
mutableStateOf(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isDeveloperModeEnabled by preferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
|
val isDeveloperModeEnabled by appPreferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
|
||||||
|
|
||||||
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,
|
||||||
userCanRedact = event.canRedact,
|
userCanRedactOwn = event.canRedactOwn,
|
||||||
|
userCanRedactOther = event.canRedactOther,
|
||||||
userCanSendMessage = event.canSendMessage,
|
userCanSendMessage = event.canSendMessage,
|
||||||
userCanSendReaction = event.canSendReaction,
|
userCanSendReaction = event.canSendReaction,
|
||||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||||
|
|
@ -73,13 +74,15 @@ class ActionListPresenter @Inject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.computeForMessage(
|
private fun CoroutineScope.computeForMessage(
|
||||||
timelineItem: TimelineItem.Event,
|
timelineItem: TimelineItem.Event,
|
||||||
userCanRedact: Boolean,
|
userCanRedactOwn: Boolean,
|
||||||
|
userCanRedactOther: Boolean,
|
||||||
userCanSendMessage: Boolean,
|
userCanSendMessage: Boolean,
|
||||||
userCanSendReaction: Boolean,
|
userCanSendReaction: Boolean,
|
||||||
isDeveloperModeEnabled: Boolean,
|
isDeveloperModeEnabled: Boolean,
|
||||||
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 =
|
||||||
when (timelineItem.content) {
|
when (timelineItem.content) {
|
||||||
is TimelineItemRedactedContent -> {
|
is TimelineItemRedactedContent -> {
|
||||||
|
|
@ -98,8 +101,10 @@ class ActionListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TimelineItemPollContent -> {
|
is TimelineItemPollContent -> {
|
||||||
|
val canEndPoll = timelineItem.isRemote &&
|
||||||
|
!timelineItem.content.isEnded &&
|
||||||
|
(timelineItem.isMine || canRedact)
|
||||||
buildList {
|
buildList {
|
||||||
val isMineOrCanRedact = timelineItem.isMine || userCanRedact
|
|
||||||
if (timelineItem.isRemote) {
|
if (timelineItem.isRemote) {
|
||||||
// Can only reply or forward messages already uploaded to the server
|
// Can only reply or forward messages already uploaded to the server
|
||||||
add(TimelineItemAction.Reply)
|
add(TimelineItemAction.Reply)
|
||||||
|
|
@ -107,7 +112,7 @@ class ActionListPresenter @Inject constructor(
|
||||||
if (timelineItem.isRemote && timelineItem.isEditable) {
|
if (timelineItem.isRemote && timelineItem.isEditable) {
|
||||||
add(TimelineItemAction.Edit)
|
add(TimelineItemAction.Edit)
|
||||||
}
|
}
|
||||||
if (timelineItem.isRemote && !timelineItem.content.isEnded && isMineOrCanRedact) {
|
if (canEndPoll) {
|
||||||
add(TimelineItemAction.EndPoll)
|
add(TimelineItemAction.EndPoll)
|
||||||
}
|
}
|
||||||
if (timelineItem.content.canBeCopied()) {
|
if (timelineItem.content.canBeCopied()) {
|
||||||
|
|
@ -119,7 +124,7 @@ class ActionListPresenter @Inject constructor(
|
||||||
if (!timelineItem.isMine) {
|
if (!timelineItem.isMine) {
|
||||||
add(TimelineItemAction.ReportContent)
|
add(TimelineItemAction.ReportContent)
|
||||||
}
|
}
|
||||||
if (isMineOrCanRedact) {
|
if (canRedact) {
|
||||||
add(TimelineItemAction.Redact)
|
add(TimelineItemAction.Redact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +141,7 @@ class ActionListPresenter @Inject constructor(
|
||||||
if (!timelineItem.isMine) {
|
if (!timelineItem.isMine) {
|
||||||
add(TimelineItemAction.ReportContent)
|
add(TimelineItemAction.ReportContent)
|
||||||
}
|
}
|
||||||
if (timelineItem.isMine || userCanRedact) {
|
if (canRedact) {
|
||||||
add(TimelineItemAction.Redact)
|
add(TimelineItemAction.Redact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +174,7 @@ class ActionListPresenter @Inject constructor(
|
||||||
if (!timelineItem.isMine) {
|
if (!timelineItem.isMine) {
|
||||||
add(TimelineItemAction.ReportContent)
|
add(TimelineItemAction.ReportContent)
|
||||||
}
|
}
|
||||||
if (timelineItem.isMine || userCanRedact) {
|
if (canRedact) {
|
||||||
add(TimelineItemAction.Redact)
|
add(TimelineItemAction.Redact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
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.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -57,8 +59,9 @@ fun AttachmentsPreviewView(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.sendActionState is SendActionState.Done) {
|
if (state.sendActionState is SendActionState.Done) {
|
||||||
|
val latestOnDismiss by rememberUpdatedState(onDismiss)
|
||||||
LaunchedEffect(state.sendActionState) {
|
LaunchedEffect(state.sendActionState) {
|
||||||
onDismiss()
|
latestOnDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,6 @@ package io.element.android.features.messages.impl.forward
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
|
||||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
|
@ -51,30 +48,3 @@ fun aForwardMessagesState(
|
||||||
forwardingSucceeded = forwardingSucceeded,
|
forwardingSucceeded = forwardingSucceeded,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun aForwardMessagesRoomList() = persistentListOf(
|
|
||||||
aRoomDetailsState(),
|
|
||||||
aRoomDetailsState(roomId = RoomId("!room2:domain"), canonicalAlias = "#element-x-room:matrix.org"),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun aRoomDetailsState(
|
|
||||||
roomId: RoomId = RoomId("!room:domain"),
|
|
||||||
name: String = "roomName",
|
|
||||||
canonicalAlias: String? = null,
|
|
||||||
isDirect: Boolean = true,
|
|
||||||
avatarURLString: String? = null,
|
|
||||||
lastMessage: RoomMessage? = null,
|
|
||||||
lastMessageTimestamp: Long? = null,
|
|
||||||
unreadNotificationCount: Int = 0,
|
|
||||||
inviter: RoomMember? = null,
|
|
||||||
) = RoomSummaryDetails(
|
|
||||||
roomId = roomId,
|
|
||||||
name = name,
|
|
||||||
canonicalAlias = canonicalAlias,
|
|
||||||
isDirect = isDirect,
|
|
||||||
avatarURLString = avatarURLString,
|
|
||||||
lastMessage = lastMessage,
|
|
||||||
lastMessageTimestamp = lastMessageTimestamp,
|
|
||||||
unreadNotificationCount = unreadNotificationCount,
|
|
||||||
inviter = inviter,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ sealed interface MessageComposerEvents {
|
||||||
data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents
|
data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents
|
||||||
data object CancelSendAttachment : MessageComposerEvents
|
data object CancelSendAttachment : MessageComposerEvents
|
||||||
data class Error(val error: Throwable) : MessageComposerEvents
|
data class Error(val error: Throwable) : MessageComposerEvents
|
||||||
|
data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents
|
||||||
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
|
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
|
||||||
data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents
|
data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -207,6 +208,15 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
// Declare that the user is not typing anymore when the composer is disposed
|
||||||
|
onDispose {
|
||||||
|
appCoroutineScope.launch {
|
||||||
|
room.typingNotice(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvents(event: MessageComposerEvents) {
|
fun handleEvents(event: MessageComposerEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
|
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
|
||||||
|
|
@ -299,6 +309,11 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
is MessageComposerEvents.Error -> {
|
is MessageComposerEvents.Error -> {
|
||||||
analyticsService.trackError(event.error)
|
analyticsService.trackError(event.error)
|
||||||
}
|
}
|
||||||
|
is MessageComposerEvents.TypingNotice -> {
|
||||||
|
localCoroutineScope.launch {
|
||||||
|
room.typingNotice(event.isTyping)
|
||||||
|
}
|
||||||
|
}
|
||||||
is MessageComposerEvents.SuggestionReceived -> {
|
is MessageComposerEvents.SuggestionReceived -> {
|
||||||
suggestionSearchTrigger.value = event.suggestion
|
suggestionSearchTrigger.value = event.suggestion
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,10 @@ internal fun MessageComposerView(
|
||||||
state.eventSink(MessageComposerEvents.Error(error))
|
state.eventSink(MessageComposerEvents.Error(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onTyping(typing: Boolean) {
|
||||||
|
state.eventSink(MessageComposerEvents.TypingNotice(typing))
|
||||||
|
}
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
fun onRequestFocus() {
|
fun onRequestFocus() {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
|
@ -121,6 +125,7 @@ internal fun MessageComposerView(
|
||||||
onDeleteVoiceMessage = onDeleteVoiceMessage,
|
onDeleteVoiceMessage = onDeleteVoiceMessage,
|
||||||
onSuggestionReceived = ::onSuggestionReceived,
|
onSuggestionReceived = ::onSuggestionReceived,
|
||||||
onError = ::onError,
|
onError = ::onError,
|
||||||
|
onTyping = ::onTyping,
|
||||||
currentUserId = state.currentUserId,
|
currentUserId = state.currentUserId,
|
||||||
onRichContentSelected = ::sendUri,
|
onRichContentSelected = ::sendUri,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
import io.element.android.features.poll.api.actions.EndPollAction
|
import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||||
|
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -53,6 +54,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
|
||||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
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
|
||||||
|
|
@ -72,6 +74,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
||||||
private val sendPollResponseAction: SendPollResponseAction,
|
private val sendPollResponseAction: SendPollResponseAction,
|
||||||
private val endPollAction: EndPollAction,
|
private val endPollAction: EndPollAction,
|
||||||
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -102,6 +105,8 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState()
|
val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState()
|
||||||
val keyBackupState by encryptionService.backupStateStateFlow.collectAsState()
|
val keyBackupState by encryptionService.backupStateStateFlow.collectAsState()
|
||||||
|
|
||||||
|
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
||||||
|
|
||||||
val sessionState by remember {
|
val sessionState by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
SessionState(
|
SessionState(
|
||||||
|
|
@ -111,8 +116,6 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val membersState by room.membersStateFlow.collectAsState()
|
|
||||||
|
|
||||||
fun handleEvents(event: TimelineEvents) {
|
fun handleEvents(event: TimelineEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
TimelineEvents.LoadMore -> localScope.paginateBackwards()
|
TimelineEvents.LoadMore -> localScope.paginateBackwards()
|
||||||
|
|
@ -125,7 +128,8 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
firstVisibleIndex = event.firstIndex,
|
firstVisibleIndex = event.firstIndex,
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
lastReadReceiptIndex = lastReadReceiptIndex,
|
lastReadReceiptIndex = lastReadReceiptIndex,
|
||||||
lastReadReceiptId = lastReadReceiptId
|
lastReadReceiptId = lastReadReceiptId,
|
||||||
|
readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is TimelineEvents.PollAnswerSelected -> appScope.launch {
|
is TimelineEvents.PollAnswerSelected -> appScope.launch {
|
||||||
|
|
@ -149,13 +153,12 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
timeline
|
combine(timeline.timelineItems, room.membersStateFlow) { items, membersState ->
|
||||||
.timelineItems
|
|
||||||
.onEach {
|
|
||||||
timelineItemsFactory.replaceWith(
|
timelineItemsFactory.replaceWith(
|
||||||
timelineItems = it,
|
timelineItems = items,
|
||||||
roomMembers = membersState.roomMembers().orEmpty()
|
roomMembers = membersState.roomMembers().orEmpty()
|
||||||
)
|
)
|
||||||
|
items
|
||||||
}
|
}
|
||||||
.onEach { timelineItems ->
|
.onEach { timelineItems ->
|
||||||
if (timelineItems.isEmpty()) {
|
if (timelineItems.isEmpty()) {
|
||||||
|
|
@ -225,13 +228,14 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
timelineItems: ImmutableList<TimelineItem>,
|
timelineItems: ImmutableList<TimelineItem>,
|
||||||
lastReadReceiptIndex: MutableState<Int>,
|
lastReadReceiptIndex: MutableState<Int>,
|
||||||
lastReadReceiptId: MutableState<EventId?>,
|
lastReadReceiptId: MutableState<EventId?>,
|
||||||
|
readReceiptType: ReceiptType,
|
||||||
) = launch(dispatchers.computation) {
|
) = launch(dispatchers.computation) {
|
||||||
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
|
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
|
||||||
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
|
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
|
||||||
if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) {
|
if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) {
|
||||||
lastReadReceiptIndex.value = firstVisibleIndex
|
lastReadReceiptIndex.value = firstVisibleIndex
|
||||||
lastReadReceiptId.value = eventId
|
lastReadReceiptId.value = eventId
|
||||||
timeline.sendReadReceipt(eventId = eventId, receiptType = ReceiptType.READ)
|
timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
|
|
@ -145,7 +146,7 @@ fun TimelineView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state.paginationState.beginningOfRoomReached) {
|
if (state.paginationState.beginningOfRoomReached && !state.timelineRoomInfo.isDirect) {
|
||||||
item(contentType = "BeginningOfRoomReached") {
|
item(contentType = "BeginningOfRoomReached") {
|
||||||
TimelineItemRoomBeginningView(roomName = roomName)
|
TimelineItemRoomBeginningView(roomName = roomName)
|
||||||
}
|
}
|
||||||
|
|
@ -193,10 +194,11 @@ private fun BoxScope.TimelineScrollHelper(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val latestOnScrollFinishedAt by rememberUpdatedState(onScrollFinishedAt)
|
||||||
LaunchedEffect(isScrollFinished, isTimelineEmpty) {
|
LaunchedEffect(isScrollFinished, isTimelineEmpty) {
|
||||||
if (isScrollFinished && !isTimelineEmpty) {
|
if (isScrollFinished && !isTimelineEmpty) {
|
||||||
// Notify the parent composable about the first visible item index when scrolling finishes
|
// Notify the parent composable about the first visible item index when scrolling finishes
|
||||||
onScrollFinishedAt(lazyListState.firstVisibleItemIndex)
|
latestOnScrollFinishedAt(lazyListState.firstVisibleItemIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,11 @@ package io.element.android.features.messages.impl.timeline.components.layout
|
||||||
|
|
||||||
import android.text.Layout
|
import android.text.Layout
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.Layout
|
import androidx.compose.ui.layout.Layout
|
||||||
import androidx.compose.ui.layout.SubcomposeLayout
|
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.text.TextLayoutResult
|
import androidx.compose.ui.text.TextLayoutResult
|
||||||
import androidx.compose.ui.unit.Constraints
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
|
@ -59,23 +60,27 @@ fun ContentAvoidingLayout(
|
||||||
) {
|
) {
|
||||||
val scope = remember { ContentAvoidingLayoutScopeInstance() }
|
val scope = remember { ContentAvoidingLayoutScopeInstance() }
|
||||||
|
|
||||||
SubcomposeLayout(
|
Layout(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) { constraints ->
|
content = {
|
||||||
|
scope.content()
|
||||||
|
overlay()
|
||||||
|
}
|
||||||
|
) { measurables, constraints ->
|
||||||
|
|
||||||
// Measure the `overlay` view first, in case we need to shrink the `content`
|
// Measure the `overlay` view first, in case we need to shrink the `content`
|
||||||
val overlayPlaceable = subcompose(0, overlay).first().measure(Constraints(minWidth = 0, maxWidth = constraints.maxWidth))
|
val overlayPlaceable = measurables.last().measure(Constraints(minWidth = 0, maxWidth = constraints.maxWidth))
|
||||||
val contentConstraints = if (shrinkContent) {
|
val contentConstraints = if (shrinkContent) {
|
||||||
Constraints(minWidth = 0, maxWidth = constraints.maxWidth - overlayPlaceable.width)
|
Constraints(minWidth = 0, maxWidth = constraints.maxWidth - overlayPlaceable.width)
|
||||||
} else {
|
} else {
|
||||||
Constraints(minWidth = 0, maxWidth = constraints.maxWidth)
|
Constraints(minWidth = 0, maxWidth = constraints.maxWidth)
|
||||||
}
|
}
|
||||||
val contentPlaceable = subcompose(1) { scope.content() }.first().measure(contentConstraints)
|
val contentPlaceable = measurables.first().measure(contentConstraints)
|
||||||
|
|
||||||
var layoutWidth = contentPlaceable.width
|
var layoutWidth = contentPlaceable.width
|
||||||
var layoutHeight = contentPlaceable.height
|
var layoutHeight = contentPlaceable.height
|
||||||
|
|
||||||
val data = scope.data
|
val data = scope.data.value
|
||||||
|
|
||||||
// Free space = width of the whole component - width of its non overlapping contents
|
// Free space = width of the whole component - width of its non overlapping contents
|
||||||
val freeSpace = max(contentPlaceable.width - data.nonOverlappingContentWidth, 0)
|
val freeSpace = max(contentPlaceable.width - data.nonOverlappingContentWidth, 0)
|
||||||
|
|
@ -135,13 +140,10 @@ interface ContentAvoidingLayoutScope {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ContentAvoidingLayoutScopeInstance(
|
private class ContentAvoidingLayoutScopeInstance(
|
||||||
val data: ContentAvoidingLayoutData = ContentAvoidingLayoutData(),
|
val data: MutableState<ContentAvoidingLayoutData> = mutableStateOf(ContentAvoidingLayoutData()),
|
||||||
) : ContentAvoidingLayoutScope {
|
) : ContentAvoidingLayoutScope {
|
||||||
override fun onContentLayoutChanged(data: ContentAvoidingLayoutData) {
|
override fun onContentLayoutChanged(data: ContentAvoidingLayoutData) {
|
||||||
this.data.contentWidth = data.contentWidth
|
this.data.value = data
|
||||||
this.data.contentHeight = data.contentHeight
|
|
||||||
this.data.nonOverlappingContentWidth = data.nonOverlappingContentWidth
|
|
||||||
this.data.nonOverlappingContentHeight = data.nonOverlappingContentHeight
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,16 @@ class TimelineItemsFactory @Inject constructor(
|
||||||
newTimelineItemStates.add(timelineItemState)
|
newTimelineItemStates.add(timelineItemState)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newTimelineItemStates.add(cacheItem)
|
val updatedItem = if (cacheItem is TimelineItem.Event && roomMembers.isNotEmpty()) {
|
||||||
|
eventItemFactory.update(
|
||||||
|
timelineItem = cacheItem,
|
||||||
|
receivedMatrixTimelineItem = timelineItems[index] as MatrixTimelineItem.Event,
|
||||||
|
roomMembers = roomMembers
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
cacheItem
|
||||||
|
}
|
||||||
|
newTimelineItemStates.add(updatedItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
|
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineItemContentFactory @Inject constructor(
|
class TimelineItemContentFactory @Inject constructor(
|
||||||
|
|
@ -50,7 +50,7 @@ class TimelineItemContentFactory @Inject constructor(
|
||||||
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
|
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
|
||||||
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
|
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
|
||||||
is MessageContent -> {
|
is MessageContent -> {
|
||||||
val senderDisplayName = (eventTimelineItem.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: eventTimelineItem.sender.value
|
val senderDisplayName = eventTimelineItem.senderProfile.getDisambiguatedDisplayName(eventTimelineItem.sender)
|
||||||
messageFactory.create(itemContent, senderDisplayName, eventTimelineItem.eventId)
|
messageFactory.create(itemContent, senderDisplayName, eventTimelineItem.eventId)
|
||||||
}
|
}
|
||||||
is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem)
|
is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
@ -52,21 +53,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
val currentSender = currentTimelineItem.event.sender
|
val currentSender = currentTimelineItem.event.sender
|
||||||
val groupPosition =
|
val groupPosition =
|
||||||
computeGroupPosition(currentTimelineItem, timelineItems, index)
|
computeGroupPosition(currentTimelineItem, timelineItems, index)
|
||||||
val senderDisplayName: String?
|
val (senderDisplayName, senderAvatarUrl) = currentTimelineItem.getSenderInfo()
|
||||||
val senderAvatarUrl: String?
|
|
||||||
|
|
||||||
when (val senderProfile = currentTimelineItem.event.senderProfile) {
|
|
||||||
ProfileTimelineDetails.Unavailable,
|
|
||||||
ProfileTimelineDetails.Pending,
|
|
||||||
is ProfileTimelineDetails.Error -> {
|
|
||||||
senderDisplayName = null
|
|
||||||
senderAvatarUrl = null
|
|
||||||
}
|
|
||||||
is ProfileTimelineDetails.Ready -> {
|
|
||||||
senderDisplayName = senderProfile.displayName
|
|
||||||
senderAvatarUrl = senderProfile.avatarUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||||
val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp))
|
val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp))
|
||||||
|
|
@ -100,6 +87,36 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun update(
|
||||||
|
timelineItem: TimelineItem.Event,
|
||||||
|
receivedMatrixTimelineItem: MatrixTimelineItem.Event,
|
||||||
|
roomMembers: List<RoomMember>,
|
||||||
|
): TimelineItem.Event {
|
||||||
|
return timelineItem.copy(
|
||||||
|
readReceiptState = receivedMatrixTimelineItem.computeReadReceiptState(roomMembers)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MatrixTimelineItem.Event.getSenderInfo(): Pair<String?, String?> {
|
||||||
|
val senderDisplayName: String?
|
||||||
|
val senderAvatarUrl: String?
|
||||||
|
|
||||||
|
when (val senderProfile = event.senderProfile) {
|
||||||
|
ProfileTimelineDetails.Unavailable,
|
||||||
|
ProfileTimelineDetails.Pending,
|
||||||
|
is ProfileTimelineDetails.Error -> {
|
||||||
|
senderDisplayName = null
|
||||||
|
senderAvatarUrl = null
|
||||||
|
}
|
||||||
|
is ProfileTimelineDetails.Ready -> {
|
||||||
|
senderDisplayName = senderProfile.getDisambiguatedDisplayName(event.sender)
|
||||||
|
senderAvatarUrl = senderProfile.avatarUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return senderDisplayName to senderAvatarUrl
|
||||||
|
}
|
||||||
|
|
||||||
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
|
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
|
||||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||||
var aggregatedReactions = event.reactions.map { reaction ->
|
var aggregatedReactions = event.reactions.map { reaction ->
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
<string name="screen_room_attachment_source_camera_photo">"Prendre une photo"</string>
|
<string name="screen_room_attachment_source_camera_photo">"Prendre une photo"</string>
|
||||||
<string name="screen_room_attachment_source_camera_video">"Enregistrer une vidéo"</string>
|
<string name="screen_room_attachment_source_camera_video">"Enregistrer une vidéo"</string>
|
||||||
<string name="screen_room_attachment_source_files">"Pièce jointe"</string>
|
<string name="screen_room_attachment_source_files">"Pièce jointe"</string>
|
||||||
<string name="screen_room_attachment_source_gallery">"Gallerie Photo et Vidéo"</string>
|
<string name="screen_room_attachment_source_gallery">"Galerie Photo et Vidéo"</string>
|
||||||
<string name="screen_room_attachment_source_location">"Position"</string>
|
<string name="screen_room_attachment_source_location">"Position"</string>
|
||||||
<string name="screen_room_attachment_source_poll">"Sondage"</string>
|
<string name="screen_room_attachment_source_poll">"Sondage"</string>
|
||||||
<string name="screen_room_attachment_text_formatting">"Formatage du texte"</string>
|
<string name="screen_room_attachment_text_formatting">"Formatage du texte"</string>
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
|
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
|
||||||
|
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
|
|
@ -120,7 +121,7 @@ class MessagesPresenterTest {
|
||||||
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.userHasPermissionToSendMessage).isTrue()
|
||||||
assertThat(initialState.userHasPermissionToRedact).isFalse()
|
assertThat(initialState.userHasPermissionToRedactOwn).isFalse()
|
||||||
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)
|
||||||
|
|
@ -601,14 +602,29 @@ class MessagesPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - permission to redact`() = runTest {
|
fun `present - permission to redact own`() = runTest {
|
||||||
val matrixRoom = FakeMatrixRoom(canRedact = true)
|
val matrixRoom = FakeMatrixRoom(canRedactOwn = 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.userHasPermissionToRedact }.last()
|
val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedactOwn }.last()
|
||||||
assertThat(initialState.userHasPermissionToRedact).isTrue()
|
assertThat(initialState.userHasPermissionToRedactOwn).isTrue()
|
||||||
|
assertThat(initialState.userHasPermissionToRedactOther).isFalse()
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - permission to redact other`() = runTest {
|
||||||
|
val matrixRoom = FakeMatrixRoom(canRedactOther = true)
|
||||||
|
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedactOther }.last()
|
||||||
|
assertThat(initialState.userHasPermissionToRedactOwn).isFalse()
|
||||||
|
assertThat(initialState.userHasPermissionToRedactOther).isTrue()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -649,10 +665,11 @@ class MessagesPresenterTest {
|
||||||
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
||||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||||
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
||||||
currentSessionIdHolder: CurrentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
|
|
||||||
): MessagesPresenter {
|
): MessagesPresenter {
|
||||||
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
|
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
|
||||||
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
|
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
|
||||||
|
val appPreferencesStore = InMemoryAppPreferencesStore(isRichTextEditorEnabled = true)
|
||||||
|
val sessionPreferencesStore = InMemorySessionPreferencesStore()
|
||||||
val messageComposerPresenter = MessageComposerPresenter(
|
val messageComposerPresenter = MessageComposerPresenter(
|
||||||
appCoroutineScope = this,
|
appCoroutineScope = this,
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
|
|
@ -687,14 +704,14 @@ class MessagesPresenterTest {
|
||||||
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||||
endPollAction = FakeEndPollAction(),
|
endPollAction = FakeEndPollAction(),
|
||||||
sendPollResponseAction = FakeSendPollResponseAction(),
|
sendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
)
|
)
|
||||||
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
||||||
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
||||||
return timelinePresenter
|
return timelinePresenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true)
|
val actionListPresenter = ActionListPresenter(appPreferencesStore = appPreferencesStore)
|
||||||
val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore)
|
|
||||||
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)
|
||||||
|
|
@ -714,11 +731,10 @@ class MessagesPresenterTest {
|
||||||
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
clipboardHelper = clipboardHelper,
|
clipboardHelper = clipboardHelper,
|
||||||
preferencesStore = preferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
featureFlagsService = FakeFeatureFlagService(),
|
featureFlagsService = FakeFeatureFlagService(),
|
||||||
buildMeta = aBuildMeta(),
|
buildMeta = aBuildMeta(),
|
||||||
dispatchers = coroutineDispatchers,
|
dispatchers = coroutineDispatchers,
|
||||||
currentSessionIdHolder = currentSessionIdHolder,
|
|
||||||
htmlConverterProvider = FakeHtmlConverterProvider(),
|
htmlConverterProvider = FakeHtmlConverterProvider(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ 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.test.InMemoryPreferencesStore
|
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
|
||||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -38,6 +38,7 @@ import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
@Suppress("LargeClass")
|
||||||
class ActionListPresenterTest {
|
class ActionListPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
@ -61,7 +62,15 @@ class ActionListPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent)
|
val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = false,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = 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()
|
val successState = awaitItem()
|
||||||
|
|
@ -87,7 +96,15 @@ class ActionListPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent)
|
val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = false,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = 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()
|
val successState = awaitItem()
|
||||||
|
|
@ -116,7 +133,15 @@ class ActionListPresenterTest {
|
||||||
isMine = false,
|
isMine = 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(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = false,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = 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()
|
val successState = awaitItem()
|
||||||
|
|
@ -149,7 +174,15 @@ class ActionListPresenterTest {
|
||||||
isMine = false,
|
isMine = 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(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = false, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = false,
|
||||||
|
canSendReaction = 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()
|
val successState = awaitItem()
|
||||||
|
|
@ -181,7 +214,15 @@ class ActionListPresenterTest {
|
||||||
isMine = false,
|
isMine = 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(ActionListEvents.ComputeForMessage(messageEvent, canRedact = true, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = false,
|
||||||
|
canRedactOther = true,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
|
|
@ -213,7 +254,15 @@ class ActionListPresenterTest {
|
||||||
isMine = false,
|
isMine = 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(ActionListEvents.ComputeForMessage(messageEvent, canRedact = true, canSendMessage = true, canSendReaction = false))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = false,
|
||||||
|
canRedactOther = true,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = false
|
||||||
|
)
|
||||||
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
|
|
@ -245,7 +294,15 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
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(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = 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()
|
val successState = awaitItem()
|
||||||
|
|
@ -268,6 +325,47 @@ class ActionListPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - compute for my message cannot redact`() = runTest {
|
||||||
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = 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,
|
||||||
|
canRedactOwn = false,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = 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.Copy,
|
||||||
|
TimelineItemAction.ViewSource,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@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)
|
||||||
|
|
@ -279,7 +377,15 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = aTimelineItemImageContent(),
|
content = aTimelineItemImageContent(),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = 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()
|
val successState = awaitItem()
|
||||||
|
|
@ -311,7 +417,15 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = aTimelineItemStateEventContent(),
|
content = aTimelineItemStateEventContent(),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = stateEvent,
|
||||||
|
canRedactOwn = false,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = 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()
|
val successState = awaitItem()
|
||||||
|
|
@ -341,7 +455,15 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = aTimelineItemStateEventContent(),
|
content = aTimelineItemStateEventContent(),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = stateEvent,
|
||||||
|
canRedactOwn = false,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = 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()
|
val successState = awaitItem()
|
||||||
|
|
@ -370,7 +492,15 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
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(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = 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()
|
val successState = awaitItem()
|
||||||
|
|
@ -408,10 +538,26 @@ class ActionListPresenterTest {
|
||||||
content = TimelineItemRedactedContent,
|
content = TimelineItemRedactedContent,
|
||||||
)
|
)
|
||||||
|
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = false,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
|
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
|
||||||
|
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = redactedEvent,
|
||||||
|
canRedactOwn = false,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
awaitItem().run {
|
awaitItem().run {
|
||||||
assertThat(target).isEqualTo(ActionListState.Target.None)
|
assertThat(target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
|
|
@ -432,7 +578,15 @@ class ActionListPresenterTest {
|
||||||
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(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
|
|
@ -460,7 +614,15 @@ class ActionListPresenterTest {
|
||||||
isEditable = true,
|
isEditable = true,
|
||||||
content = aTimelineItemPollContent(answerItems = aPollAnswerItemList(hasVotes = false)),
|
content = aTimelineItemPollContent(answerItems = aPollAnswerItemList(hasVotes = false)),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
|
|
@ -489,7 +651,15 @@ class ActionListPresenterTest {
|
||||||
isEditable = false,
|
isEditable = false,
|
||||||
content = aTimelineItemPollContent(answerItems = aPollAnswerItemList(hasVotes = true)),
|
content = aTimelineItemPollContent(answerItems = aPollAnswerItemList(hasVotes = true)),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
|
|
@ -517,7 +687,15 @@ class ActionListPresenterTest {
|
||||||
isEditable = false,
|
isEditable = false,
|
||||||
content = aTimelineItemPollContent(isEnded = true),
|
content = aTimelineItemPollContent(isEnded = true),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
|
|
@ -543,7 +721,15 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = aTimelineItemVoiceContent(),
|
content = aTimelineItemVoiceContent(),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
|
|
@ -561,6 +747,6 @@ class ActionListPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter {
|
private fun createActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter {
|
||||||
val preferencesStore = InMemoryPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
|
val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
|
||||||
return ActionListPresenter(preferencesStore = preferencesStore)
|
return ActionListPresenter(appPreferencesStore = preferencesStore)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
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.room.aRoomSummaryDetail
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -54,7 +54,7 @@ class ForwardMessagesPresenterTests {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val summary = aRoomSummaryDetail()
|
val summary = aRoomSummaryDetails()
|
||||||
presenter.onRoomSelected(listOf(summary.roomId))
|
presenter.onRoomSelected(listOf(summary.roomId))
|
||||||
val forwardingState = awaitItem()
|
val forwardingState = awaitItem()
|
||||||
assertThat(forwardingState.isForwarding).isTrue()
|
assertThat(forwardingState.isForwarding).isTrue()
|
||||||
|
|
@ -74,7 +74,7 @@ class ForwardMessagesPresenterTests {
|
||||||
// Test failed forwarding
|
// Test failed forwarding
|
||||||
room.givenForwardEventResult(Result.failure(Throwable("error")))
|
room.givenForwardEventResult(Result.failure(Throwable("error")))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val summary = aRoomSummaryDetail()
|
val summary = aRoomSummaryDetails()
|
||||||
presenter.onRoomSelected(listOf(summary.roomId))
|
presenter.onRoomSelected(listOf(summary.roomId))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val failedForwardState = awaitItem()
|
val failedForwardState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -873,6 +873,21 @@ class MessageComposerPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle typing notice event`() = runTest {
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val presenter = createPresenter(room = room, coroutineScope = this)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitFirstItem()
|
||||||
|
assertThat(room.typingRecord).isEmpty()
|
||||||
|
initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(true))
|
||||||
|
initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(false))
|
||||||
|
assertThat(room.typingRecord).isEqualTo(listOf(true, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun ReceiveTurbine<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0): MessageComposerState {
|
private suspend fun ReceiveTurbine<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0): MessageComposerState {
|
||||||
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
|
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
|
||||||
skipItems(skipCount)
|
skipItems(skipCount)
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,20 @@ import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||||
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||||
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
|
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
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.room.aRoomMember
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
|
|
@ -60,6 +66,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.Companion.seconds
|
||||||
|
|
||||||
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
||||||
|
|
||||||
|
|
@ -129,13 +136,41 @@ class TimelinePresenterTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
awaitWithLatch { latch ->
|
awaitWithLatch { latch ->
|
||||||
timeline.sendReadReceiptLatch = latch
|
timeline.sendReadReceiptLatch = latch
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
||||||
}
|
}
|
||||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(1)
|
assertThat(timeline.sentReadReceipts).isNotEmpty()
|
||||||
|
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ)
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - on scroll finished send a private read receipt if an event is before the index and public read receipts are disabled`() = runTest {
|
||||||
|
val timeline = FakeMatrixTimeline(
|
||||||
|
initialTimelineItems = listOf(
|
||||||
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
|
||||||
|
val presenter = createTimelinePresenter(
|
||||||
|
timeline = timeline,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||||
|
val initialState = awaitFirstItem()
|
||||||
|
awaitWithLatch { latch ->
|
||||||
|
timeline.sendReadReceiptLatch = latch
|
||||||
|
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
||||||
|
}
|
||||||
|
assertThat(timeline.sentReadReceipts).isNotEmpty()
|
||||||
|
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ_PRIVATE)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,13 +186,13 @@ class TimelinePresenterTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
awaitWithLatch { latch ->
|
awaitWithLatch { latch ->
|
||||||
timeline.sendReadReceiptLatch = latch
|
timeline.sendReadReceiptLatch = latch
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||||
}
|
}
|
||||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -173,13 +208,13 @@ class TimelinePresenterTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
awaitWithLatch { latch ->
|
awaitWithLatch { latch ->
|
||||||
timeline.sendReadReceiptLatch = latch
|
timeline.sendReadReceiptLatch = latch
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
||||||
}
|
}
|
||||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -353,6 +388,50 @@ class TimelinePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room member info is loaded, read receipts info should be updated`() = runTest {
|
||||||
|
val timeline = FakeMatrixTimeline(
|
||||||
|
listOf(
|
||||||
|
MatrixTimelineItem.Event(
|
||||||
|
FAKE_UNIQUE_ID,
|
||||||
|
anEventTimelineItem(
|
||||||
|
sender = A_USER_ID,
|
||||||
|
receipts = persistentListOf(
|
||||||
|
Receipt(
|
||||||
|
userId = A_USER_ID,
|
||||||
|
timestamp = 0L,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val room = FakeMatrixRoom(matrixTimeline = timeline).apply {
|
||||||
|
givenRoomMembersState(MatrixRoomMembersState.Unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
val avatarUrl = "https://domain.com/avatar.jpg"
|
||||||
|
|
||||||
|
val presenter = createTimelinePresenter(timeline, room)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = consumeItemsUntilPredicate(30.seconds) { it.timelineItems.isNotEmpty() }.last()
|
||||||
|
val event = initialState.timelineItems.first() as TimelineItem.Event
|
||||||
|
assertThat(event.senderAvatar.url).isNull()
|
||||||
|
assertThat(event.readReceiptState.receipts.first().avatarData.url).isNull()
|
||||||
|
|
||||||
|
room.givenRoomMembersState(
|
||||||
|
MatrixRoomMembersState.Ready(
|
||||||
|
persistentListOf(aRoomMember(userId = A_USER_ID, avatarUrl = avatarUrl))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val updatedEvent = awaitItem().timelineItems.first() as TimelineItem.Event
|
||||||
|
assertThat(updatedEvent.readReceiptState.receipts.first().avatarData.url).isEqualTo(avatarUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||||
// Skip 1 item if Mentions feature is enabled
|
// Skip 1 item if Mentions feature is enabled
|
||||||
if (FeatureFlags.Mentions.defaultValue) {
|
if (FeatureFlags.Mentions.defaultValue) {
|
||||||
|
|
@ -363,15 +442,17 @@ class TimelinePresenterTest {
|
||||||
|
|
||||||
private fun TestScope.createTimelinePresenter(
|
private fun TestScope.createTimelinePresenter(
|
||||||
timeline: MatrixTimeline = FakeMatrixTimeline(),
|
timeline: MatrixTimeline = FakeMatrixTimeline(),
|
||||||
|
room: FakeMatrixRoom = FakeMatrixRoom(matrixTimeline = timeline),
|
||||||
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(),
|
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(),
|
||||||
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||||
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
||||||
endPollAction: EndPollAction = FakeEndPollAction(),
|
endPollAction: EndPollAction = FakeEndPollAction(),
|
||||||
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
|
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
): TimelinePresenter {
|
): TimelinePresenter {
|
||||||
return TimelinePresenter(
|
return TimelinePresenter(
|
||||||
timelineItemsFactory = timelineItemsFactory,
|
timelineItemsFactory = timelineItemsFactory,
|
||||||
room = FakeMatrixRoom(matrixTimeline = timeline),
|
room = room,
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
appScope = this,
|
appScope = this,
|
||||||
navigator = messagesNavigator,
|
navigator = messagesNavigator,
|
||||||
|
|
@ -380,6 +461,7 @@ class TimelinePresenterTest {
|
||||||
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
||||||
endPollAction = endPollAction,
|
endPollAction = endPollAction,
|
||||||
sendPollResponseAction = sendPollResponseAction,
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,5 +33,6 @@ interface OnBoardingEntryPoint : FeatureEntryPoint {
|
||||||
fun onSignUp()
|
fun onSignUp()
|
||||||
fun onSignIn()
|
fun onSignIn()
|
||||||
fun onOpenDeveloperSettings()
|
fun onOpenDeveloperSettings()
|
||||||
|
fun onReportProblem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,10 @@ class OnBoardingNode @AssistedInject constructor(
|
||||||
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onOpenDeveloperSettings() }
|
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onOpenDeveloperSettings() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onReportProblem() {
|
||||||
|
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onReportProblem() }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
@ -59,6 +63,7 @@ class OnBoardingNode @AssistedInject constructor(
|
||||||
onCreateAccount = ::onSignUp,
|
onCreateAccount = ::onSignUp,
|
||||||
onSignInWithQrCode = { /* Not supported yet */ },
|
onSignInWithQrCode = { /* Not supported yet */ },
|
||||||
onOpenDeveloperSettings = ::onOpenDeveloperSettings,
|
onOpenDeveloperSettings = ::onOpenDeveloperSettings,
|
||||||
|
onReportProblem = ::onReportProblem,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.onboarding.impl
|
package io.element.android.features.onboarding.impl
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
|
@ -65,6 +66,7 @@ fun OnBoardingView(
|
||||||
onSignIn: () -> Unit,
|
onSignIn: () -> Unit,
|
||||||
onCreateAccount: () -> Unit,
|
onCreateAccount: () -> Unit,
|
||||||
onOpenDeveloperSettings: () -> Unit,
|
onOpenDeveloperSettings: () -> Unit,
|
||||||
|
onReportProblem: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
OnBoardingPage(
|
OnBoardingPage(
|
||||||
|
|
@ -81,6 +83,7 @@ fun OnBoardingView(
|
||||||
onSignInWithQrCode = onSignInWithQrCode,
|
onSignInWithQrCode = onSignInWithQrCode,
|
||||||
onSignIn = onSignIn,
|
onSignIn = onSignIn,
|
||||||
onCreateAccount = onCreateAccount,
|
onCreateAccount = onCreateAccount,
|
||||||
|
onReportProblem = onReportProblem,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -154,6 +157,7 @@ private fun OnBoardingButtons(
|
||||||
onSignInWithQrCode: () -> Unit,
|
onSignInWithQrCode: () -> Unit,
|
||||||
onSignIn: () -> Unit,
|
onSignIn: () -> Unit,
|
||||||
onCreateAccount: () -> Unit,
|
onCreateAccount: () -> Unit,
|
||||||
|
onReportProblem: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
ButtonColumnMolecule(modifier = modifier) {
|
ButtonColumnMolecule(modifier = modifier) {
|
||||||
|
|
@ -187,6 +191,15 @@ private fun OnBoardingButtons(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
// Add a report problem text button. Use a Text since we need a special theme here.
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.clickable(onClick = onReportProblem),
|
||||||
|
text = stringResource(id = CommonStrings.common_report_a_problem),
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,6 +213,7 @@ internal fun OnBoardingScreenPreview(
|
||||||
onSignInWithQrCode = {},
|
onSignInWithQrCode = {},
|
||||||
onSignIn = {},
|
onSignIn = {},
|
||||||
onCreateAccount = {},
|
onCreateAccount = {},
|
||||||
onOpenDeveloperSettings = {}
|
onOpenDeveloperSettings = {},
|
||||||
|
onReportProblem = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.indicator.impl)
|
testImplementation(projects.libraries.indicator.impl)
|
||||||
testImplementation(projects.features.logout.impl)
|
testImplementation(projects.features.logout.impl)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
|
testImplementation(projects.services.toolbox.test)
|
||||||
testImplementation(projects.features.analytics.impl)
|
testImplementation(projects.features.analytics.impl)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.element.android.compound.theme.Theme
|
||||||
sealed interface AdvancedSettingsEvents {
|
sealed interface AdvancedSettingsEvents {
|
||||||
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||||
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||||
|
data class SetSendPublicReadReceiptsEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||||
data object ChangeTheme : AdvancedSettingsEvents
|
data object ChangeTheme : AdvancedSettingsEvents
|
||||||
data object CancelChangeTheme : AdvancedSettingsEvents
|
data object CancelChangeTheme : AdvancedSettingsEvents
|
||||||
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
||||||
|
|
|
||||||
|
|
@ -25,40 +25,48 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.compound.theme.Theme
|
import io.element.android.compound.theme.Theme
|
||||||
import io.element.android.compound.theme.mapToTheme
|
import io.element.android.compound.theme.mapToTheme
|
||||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
|
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AdvancedSettingsPresenter @Inject constructor(
|
class AdvancedSettingsPresenter @Inject constructor(
|
||||||
private val preferencesStore: PreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
) : Presenter<AdvancedSettingsState> {
|
) : Presenter<AdvancedSettingsState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): AdvancedSettingsState {
|
override fun present(): AdvancedSettingsState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val isRichTextEditorEnabled by preferencesStore
|
val isRichTextEditorEnabled by appPreferencesStore
|
||||||
.isRichTextEditorEnabledFlow()
|
.isRichTextEditorEnabledFlow()
|
||||||
.collectAsState(initial = false)
|
.collectAsState(initial = false)
|
||||||
val isDeveloperModeEnabled by preferencesStore
|
val isDeveloperModeEnabled by appPreferencesStore
|
||||||
.isDeveloperModeEnabledFlow()
|
.isDeveloperModeEnabledFlow()
|
||||||
.collectAsState(initial = false)
|
.collectAsState(initial = false)
|
||||||
|
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore
|
||||||
|
.isSendPublicReadReceiptsEnabled()
|
||||||
|
.collectAsState(initial = true)
|
||||||
val theme by remember {
|
val theme by remember {
|
||||||
preferencesStore.getThemeFlow().mapToTheme()
|
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||||
}
|
}
|
||||||
.collectAsState(initial = Theme.System)
|
.collectAsState(initial = Theme.System)
|
||||||
var showChangeThemeDialog by remember { mutableStateOf(false) }
|
var showChangeThemeDialog by remember { mutableStateOf(false) }
|
||||||
fun handleEvents(event: AdvancedSettingsEvents) {
|
fun handleEvents(event: AdvancedSettingsEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch {
|
is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch {
|
||||||
preferencesStore.setRichTextEditorEnabled(event.enabled)
|
appPreferencesStore.setRichTextEditorEnabled(event.enabled)
|
||||||
}
|
}
|
||||||
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
|
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
|
||||||
preferencesStore.setDeveloperModeEnabled(event.enabled)
|
appPreferencesStore.setDeveloperModeEnabled(event.enabled)
|
||||||
|
}
|
||||||
|
is AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled -> localCoroutineScope.launch {
|
||||||
|
sessionPreferencesStore.setSendPublicReadReceipts(event.enabled)
|
||||||
}
|
}
|
||||||
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
|
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
|
||||||
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
|
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
|
||||||
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
|
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
|
||||||
preferencesStore.setTheme(event.theme.name)
|
appPreferencesStore.setTheme(event.theme.name)
|
||||||
showChangeThemeDialog = false
|
showChangeThemeDialog = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,6 +75,7 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||||
return AdvancedSettingsState(
|
return AdvancedSettingsState(
|
||||||
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
||||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||||
|
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
|
||||||
theme = theme,
|
theme = theme,
|
||||||
showChangeThemeDialog = showChangeThemeDialog,
|
showChangeThemeDialog = showChangeThemeDialog,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.element.android.compound.theme.Theme
|
||||||
data class AdvancedSettingsState(
|
data class AdvancedSettingsState(
|
||||||
val isRichTextEditorEnabled: Boolean,
|
val isRichTextEditorEnabled: Boolean,
|
||||||
val isDeveloperModeEnabled: Boolean,
|
val isDeveloperModeEnabled: Boolean,
|
||||||
|
val isSendPublicReadReceiptsEnabled: Boolean,
|
||||||
val theme: Theme,
|
val theme: Theme,
|
||||||
val showChangeThemeDialog: Boolean,
|
val showChangeThemeDialog: Boolean,
|
||||||
val eventSink: (AdvancedSettingsEvents) -> Unit
|
val eventSink: (AdvancedSettingsEvents) -> Unit
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,19 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
|
||||||
aAdvancedSettingsState(isRichTextEditorEnabled = true),
|
aAdvancedSettingsState(isRichTextEditorEnabled = true),
|
||||||
aAdvancedSettingsState(isDeveloperModeEnabled = true),
|
aAdvancedSettingsState(isDeveloperModeEnabled = true),
|
||||||
aAdvancedSettingsState(showChangeThemeDialog = true),
|
aAdvancedSettingsState(showChangeThemeDialog = true),
|
||||||
|
aAdvancedSettingsState(isSendPublicReadReceiptsEnabled = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aAdvancedSettingsState(
|
fun aAdvancedSettingsState(
|
||||||
isRichTextEditorEnabled: Boolean = false,
|
isRichTextEditorEnabled: Boolean = false,
|
||||||
isDeveloperModeEnabled: Boolean = false,
|
isDeveloperModeEnabled: Boolean = false,
|
||||||
|
isSendPublicReadReceiptsEnabled: Boolean = false,
|
||||||
showChangeThemeDialog: Boolean = false,
|
showChangeThemeDialog: Boolean = false,
|
||||||
) = AdvancedSettingsState(
|
) = AdvancedSettingsState(
|
||||||
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
||||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||||
|
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
|
||||||
theme = Theme.System,
|
theme = Theme.System,
|
||||||
showChangeThemeDialog = showChangeThemeDialog,
|
showChangeThemeDialog = showChangeThemeDialog,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,8 @@ fun AdvancedSettingsView(
|
||||||
},
|
},
|
||||||
trailingContent = ListItemContent.Switch(
|
trailingContent = ListItemContent.Switch(
|
||||||
checked = state.isRichTextEditorEnabled,
|
checked = state.isRichTextEditorEnabled,
|
||||||
onChange = { state.eventSink(AdvancedSettingsEvents.SetRichTextEditorEnabled(it)) },
|
|
||||||
),
|
),
|
||||||
|
onClick = { state.eventSink(AdvancedSettingsEvents.SetRichTextEditorEnabled(!state.isRichTextEditorEnabled)) }
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
|
|
@ -78,8 +78,20 @@ fun AdvancedSettingsView(
|
||||||
},
|
},
|
||||||
trailingContent = ListItemContent.Switch(
|
trailingContent = ListItemContent.Switch(
|
||||||
checked = state.isDeveloperModeEnabled,
|
checked = state.isDeveloperModeEnabled,
|
||||||
onChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) },
|
|
||||||
),
|
),
|
||||||
|
onClick = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(!state.isDeveloperModeEnabled)) }
|
||||||
|
)
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = stringResource(id = R.string.screen_advanced_settings_send_read_receipts))
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(text = stringResource(id = R.string.screen_advanced_settings_send_read_receipts_description))
|
||||||
|
},
|
||||||
|
trailingContent = ListItemContent.Switch(
|
||||||
|
checked = state.isSendPublicReadReceiptsEnabled,
|
||||||
|
),
|
||||||
|
onClick = { state.eventSink(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(!state.isSendPublicReadReceiptsEnabled)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import io.element.android.appconfig.ElementCallConfig
|
import io.element.android.appconfig.ElementCallConfig
|
||||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
||||||
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
|
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
|
||||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
|
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
|
||||||
|
|
@ -51,7 +51,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
|
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
|
||||||
private val clearCacheUseCase: ClearCacheUseCase,
|
private val clearCacheUseCase: ClearCacheUseCase,
|
||||||
private val rageshakePresenter: RageshakePreferencesPresenter,
|
private val rageshakePresenter: RageshakePreferencesPresenter,
|
||||||
private val preferencesStore: PreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
) : Presenter<DeveloperSettingsState> {
|
) : Presenter<DeveloperSettingsState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): DeveloperSettingsState {
|
override fun present(): DeveloperSettingsState {
|
||||||
|
|
@ -69,7 +69,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
val clearCacheAction = remember {
|
val clearCacheAction = remember {
|
||||||
mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
||||||
}
|
}
|
||||||
val customElementCallBaseUrl by preferencesStore
|
val customElementCallBaseUrl by appPreferencesStore
|
||||||
.getCustomElementCallBaseUrlFlow()
|
.getCustomElementCallBaseUrlFlow()
|
||||||
.collectAsState(initial = null)
|
.collectAsState(initial = null)
|
||||||
|
|
||||||
|
|
@ -100,7 +100,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
is DeveloperSettingsEvents.SetCustomElementCallBaseUrl -> coroutineScope.launch {
|
is DeveloperSettingsEvents.SetCustomElementCallBaseUrl -> coroutineScope.launch {
|
||||||
// If the URL is either empty or the default one, we want to save 'null' to remove the custom URL
|
// If the URL is either empty or the default one, we want to save 'null' to remove the custom URL
|
||||||
val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() && it != ElementCallConfig.DEFAULT_BASE_URL }
|
val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() && it != ElementCallConfig.DEFAULT_BASE_URL }
|
||||||
preferencesStore.setCustomElementCallBaseUrl(urlToSave)
|
appPreferencesStore.setCustomElementCallBaseUrl(urlToSave)
|
||||||
}
|
}
|
||||||
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
|
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvider<EditDefaultNotificationSettingState> {
|
open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvider<EditDefaultNotificationSettingState> {
|
||||||
|
|
@ -49,14 +49,12 @@ private fun anEditDefaultNotificationSettingsState(
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun aRoomSummary() = RoomSummary.Filled(
|
private fun aRoomSummary() = RoomSummary.Filled(
|
||||||
RoomSummaryDetails(
|
aRoomSummaryDetails(
|
||||||
roomId = RoomId("!roomId:domain"),
|
roomId = RoomId("!roomId:domain"),
|
||||||
name = "Room",
|
name = "Room",
|
||||||
avatarURLString = null,
|
avatarUrl = null,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
lastMessage = null,
|
lastMessage = null,
|
||||||
lastMessageTimestamp = null,
|
|
||||||
unreadNotificationCount = 0,
|
|
||||||
notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ fun EditDefaultNotificationSettingView(
|
||||||
if (state.roomsWithUserDefinedMode.isNotEmpty()) {
|
if (state.roomsWithUserDefinedMode.isNotEmpty()) {
|
||||||
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_edit_custom_settings_section_title)) {
|
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_edit_custom_settings_section_title)) {
|
||||||
state.roomsWithUserDefinedMode.forEach { summary ->
|
state.roomsWithUserDefinedMode.forEach { summary ->
|
||||||
val subtitle = when (summary.details.notificationMode) {
|
val subtitle = when (summary.details.userDefinedNotificationMode) {
|
||||||
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_notification_settings_edit_mode_all_messages)
|
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_notification_settings_edit_mode_all_messages)
|
||||||
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> {
|
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> {
|
||||||
stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords)
|
stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords)
|
||||||
|
|
@ -95,7 +95,7 @@ fun EditDefaultNotificationSettingView(
|
||||||
val avatarData = AvatarData(
|
val avatarData = AvatarData(
|
||||||
id = summary.identifier(),
|
id = summary.identifier(),
|
||||||
name = summary.details.name,
|
name = summary.details.name,
|
||||||
url = summary.details.avatarURLString,
|
url = summary.details.avatarUrl,
|
||||||
size = AvatarSize.CustomRoomNotificationSetting,
|
size = AvatarSize.CustomRoomNotificationSetting,
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,16 @@ class DefaultVersionFormatter @Inject constructor(
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
) : VersionFormatter {
|
) : VersionFormatter {
|
||||||
override fun get(): String {
|
override fun get(): String {
|
||||||
return stringProvider.getString(
|
val base = stringProvider.getString(
|
||||||
CommonStrings.settings_version_number,
|
CommonStrings.settings_version_number,
|
||||||
buildMeta.versionName,
|
buildMeta.versionName,
|
||||||
buildMeta.versionCode.toString()
|
buildMeta.versionCode.toString()
|
||||||
)
|
)
|
||||||
}
|
return if (buildMeta.gitBranchName == "main") {
|
||||||
}
|
base
|
||||||
|
} else {
|
||||||
class FakeVersionFormatter : VersionFormatter {
|
// In case of a build not from main, we display the branch name and the revision
|
||||||
override fun get(): String {
|
"$base\n${buildMeta.gitBranchName}\n${buildMeta.gitRevision}"
|
||||||
return "A Version"
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
<string name="screen_advanced_settings_developer_mode">"Vývojářský režim"</string>
|
<string name="screen_advanced_settings_developer_mode">"Vývojářský režim"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode_description">"Povolením získáte přístup k funkcím a funkcím pro vývojáře."</string>
|
<string name="screen_advanced_settings_developer_mode_description">"Povolením získáte přístup k funkcím a funkcím pro vývojáře."</string>
|
||||||
<string name="screen_advanced_settings_rich_text_editor_description">"Vypněte editor formátovaného textu pro ruční zadání Markdown."</string>
|
<string name="screen_advanced_settings_rich_text_editor_description">"Vypněte editor formátovaného textu pro ruční zadání Markdown."</string>
|
||||||
|
<string name="screen_advanced_settings_send_read_receipts">"Potvrzení o přečtení"</string>
|
||||||
|
<string name="screen_advanced_settings_send_read_receipts_description">"Pokud je vypnuto, potvrzení o přečtení se nikomu neodesílají. Stále budete dostávat potvrzení o přečtení od ostatních uživatelů."</string>
|
||||||
<string name="screen_advanced_settings_view_source_description">"Povolit možnost zobrazení zdroje zprávy na časové ose."</string>
|
<string name="screen_advanced_settings_view_source_description">"Povolit možnost zobrazení zdroje zprávy na časové ose."</string>
|
||||||
<string name="screen_edit_profile_display_name">"Zobrazované jméno"</string>
|
<string name="screen_edit_profile_display_name">"Zobrazované jméno"</string>
|
||||||
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované jméno"</string>
|
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované jméno"</string>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
<string name="screen_advanced_settings_developer_mode">"Mode développeur"</string>
|
<string name="screen_advanced_settings_developer_mode">"Mode développeur"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode_description">"Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs."</string>
|
<string name="screen_advanced_settings_developer_mode_description">"Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs."</string>
|
||||||
<string name="screen_advanced_settings_rich_text_editor_description">"Désactivez l’éditeur de texte enrichi pour saisir manuellement du Markdown."</string>
|
<string name="screen_advanced_settings_rich_text_editor_description">"Désactivez l’éditeur de texte enrichi pour saisir manuellement du Markdown."</string>
|
||||||
|
<string name="screen_advanced_settings_send_read_receipts">"Accusés de lecture"</string>
|
||||||
|
<string name="screen_advanced_settings_send_read_receipts_description">"En cas de désactivation, vos accusés de lecture ne seront pas envoyés aux autres membres. Vous verrez toujours les accusés des autres membres."</string>
|
||||||
<string name="screen_advanced_settings_view_source_description">"Activer cette option pour pouvoir voir la source des messages dans la discussion."</string>
|
<string name="screen_advanced_settings_view_source_description">"Activer cette option pour pouvoir voir la source des messages dans la discussion."</string>
|
||||||
<string name="screen_edit_profile_display_name">"Pseudonyme"</string>
|
<string name="screen_edit_profile_display_name">"Pseudonyme"</string>
|
||||||
<string name="screen_edit_profile_display_name_placeholder">"Votre pseudonyme"</string>
|
<string name="screen_edit_profile_display_name_placeholder">"Votre pseudonyme"</string>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
<string name="screen_advanced_settings_developer_mode">"Vývojársky režim"</string>
|
<string name="screen_advanced_settings_developer_mode">"Vývojársky režim"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode_description">"Umožniť prístup k možnostiam a funkciám pre vývojárov."</string>
|
<string name="screen_advanced_settings_developer_mode_description">"Umožniť prístup k možnostiam a funkciám pre vývojárov."</string>
|
||||||
<string name="screen_advanced_settings_rich_text_editor_description">"Vypnite rozšírený textový editor na ručné písanie Markdown."</string>
|
<string name="screen_advanced_settings_rich_text_editor_description">"Vypnite rozšírený textový editor na ručné písanie Markdown."</string>
|
||||||
|
<string name="screen_advanced_settings_send_read_receipts">"Potvrdenia o prečítaní"</string>
|
||||||
|
<string name="screen_advanced_settings_send_read_receipts_description">"Ak je táto funkcia vypnutá, vaše potvrdenia o prečítaní sa nebudú nikomu odosielať. Stále budete dostávať potvrdenia o prečítaní od ostatných používateľov."</string>
|
||||||
<string name="screen_advanced_settings_view_source_description">"Povoliť možnosť zobrazenia zdroja správy na časovej osi."</string>
|
<string name="screen_advanced_settings_view_source_description">"Povoliť možnosť zobrazenia zdroja správy na časovej osi."</string>
|
||||||
<string name="screen_edit_profile_display_name">"Zobrazované meno"</string>
|
<string name="screen_edit_profile_display_name">"Zobrazované meno"</string>
|
||||||
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované meno"</string>
|
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované meno"</string>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
<string name="screen_advanced_settings_developer_mode">"Developer mode"</string>
|
<string name="screen_advanced_settings_developer_mode">"Developer mode"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode_description">"Enable to have access to features and functionality for developers."</string>
|
<string name="screen_advanced_settings_developer_mode_description">"Enable to have access to features and functionality for developers."</string>
|
||||||
<string name="screen_advanced_settings_rich_text_editor_description">"Disable the rich text editor to type Markdown manually."</string>
|
<string name="screen_advanced_settings_rich_text_editor_description">"Disable the rich text editor to type Markdown manually."</string>
|
||||||
|
<string name="screen_advanced_settings_send_read_receipts">"Read receipts"</string>
|
||||||
|
<string name="screen_advanced_settings_send_read_receipts_description">"If turned off, your read receipts won\'t be sent to anyone. You will still receive read receipts from other users."</string>
|
||||||
<string name="screen_advanced_settings_view_source_description">"Enable option to view message source in the timeline."</string>
|
<string name="screen_advanced_settings_view_source_description">"Enable option to view message source in the timeline."</string>
|
||||||
<string name="screen_edit_profile_display_name">"Display name"</string>
|
<string name="screen_edit_profile_display_name">"Display name"</string>
|
||||||
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string>
|
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ 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.compound.theme.Theme
|
import io.element.android.compound.theme.Theme
|
||||||
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
|
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
|
||||||
|
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -34,8 +35,7 @@ class AdvancedSettingsPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val store = InMemoryPreferencesStore()
|
val presenter = createAdvancedSettingsPresenter()
|
||||||
val presenter = AdvancedSettingsPresenter(store)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -43,14 +43,14 @@ class AdvancedSettingsPresenterTest {
|
||||||
assertThat(initialState.isDeveloperModeEnabled).isFalse()
|
assertThat(initialState.isDeveloperModeEnabled).isFalse()
|
||||||
assertThat(initialState.isRichTextEditorEnabled).isFalse()
|
assertThat(initialState.isRichTextEditorEnabled).isFalse()
|
||||||
assertThat(initialState.showChangeThemeDialog).isFalse()
|
assertThat(initialState.showChangeThemeDialog).isFalse()
|
||||||
|
assertThat(initialState.isSendPublicReadReceiptsEnabled).isTrue()
|
||||||
assertThat(initialState.theme).isEqualTo(Theme.System)
|
assertThat(initialState.theme).isEqualTo(Theme.System)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - developer mode on off`() = runTest {
|
fun `present - developer mode on off`() = runTest {
|
||||||
val store = InMemoryPreferencesStore()
|
val presenter = createAdvancedSettingsPresenter()
|
||||||
val presenter = AdvancedSettingsPresenter(store)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -65,8 +65,7 @@ class AdvancedSettingsPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - rich text editor on off`() = runTest {
|
fun `present - rich text editor on off`() = runTest {
|
||||||
val store = InMemoryPreferencesStore()
|
val presenter = createAdvancedSettingsPresenter()
|
||||||
val presenter = AdvancedSettingsPresenter(store)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -79,10 +78,24 @@ class AdvancedSettingsPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send public read receipts off on`() = runTest {
|
||||||
|
val presenter = createAdvancedSettingsPresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitLastSequentialItem()
|
||||||
|
assertThat(initialState.isSendPublicReadReceiptsEnabled).isTrue()
|
||||||
|
initialState.eventSink.invoke(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(false))
|
||||||
|
assertThat(awaitItem().isSendPublicReadReceiptsEnabled).isFalse()
|
||||||
|
initialState.eventSink.invoke(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(true))
|
||||||
|
assertThat(awaitItem().isSendPublicReadReceiptsEnabled).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - change theme`() = runTest {
|
fun `present - change theme`() = runTest {
|
||||||
val store = InMemoryPreferencesStore()
|
val presenter = createAdvancedSettingsPresenter()
|
||||||
val presenter = AdvancedSettingsPresenter(store)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -102,4 +115,12 @@ class AdvancedSettingsPresenterTest {
|
||||||
assertThat(withNewTheme.theme).isEqualTo(Theme.Light)
|
assertThat(withNewTheme.theme).isEqualTo(Theme.Light)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createAdvancedSettingsPresenter(
|
||||||
|
appPreferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
|
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
|
) = AdvancedSettingsPresenter(
|
||||||
|
appPreferencesStore = appPreferencesStore,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataSto
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
|
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -114,7 +114,7 @@ class DeveloperSettingsPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - custom element call base url`() = runTest {
|
fun `present - custom element call base url`() = runTest {
|
||||||
val preferencesStore = InMemoryPreferencesStore()
|
val preferencesStore = InMemoryAppPreferencesStore()
|
||||||
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
|
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -149,14 +149,14 @@ class DeveloperSettingsPresenterTest {
|
||||||
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),
|
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),
|
||||||
clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(),
|
clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(),
|
||||||
rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()),
|
rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()),
|
||||||
preferencesStore: InMemoryPreferencesStore = InMemoryPreferencesStore(),
|
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
): DeveloperSettingsPresenter {
|
): DeveloperSettingsPresenter {
|
||||||
return DeveloperSettingsPresenter(
|
return DeveloperSettingsPresenter(
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
computeCacheSizeUseCase = cacheSizeUseCase,
|
computeCacheSizeUseCase = cacheSizeUseCase,
|
||||||
clearCacheUseCase = clearCacheUseCase,
|
clearCacheUseCase = clearCacheUseCase,
|
||||||
rageshakePresenter = rageshakePresenter,
|
rageshakePresenter = rageshakePresenter,
|
||||||
preferencesStore = preferencesStore,
|
appPreferencesStore = preferencesStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||||
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.room.aRoomSummaryDetail
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
|
|
@ -72,11 +72,11 @@ class EditDefaultNotificationSettingsPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
roomListService.postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail(notificationMode = RoomNotificationMode.ALL_MESSAGES))))
|
roomListService.postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetails(notificationMode = RoomNotificationMode.ALL_MESSAGES))))
|
||||||
val loadedState = consumeItemsUntilPredicate { state ->
|
val loadedState = consumeItemsUntilPredicate { state ->
|
||||||
state.roomsWithUserDefinedMode.any { it.details.notificationMode == RoomNotificationMode.ALL_MESSAGES }
|
state.roomsWithUserDefinedMode.any { it.details.userDefinedNotificationMode == RoomNotificationMode.ALL_MESSAGES }
|
||||||
}.last()
|
}.last()
|
||||||
assertThat(loadedState.roomsWithUserDefinedMode.any { it.details.notificationMode == RoomNotificationMode.ALL_MESSAGES }).isTrue()
|
assertThat(loadedState.roomsWithUserDefinedMode.any { it.details.userDefinedNotificationMode == RoomNotificationMode.ALL_MESSAGES }).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.preferences.impl.root
|
||||||
|
|
||||||
|
class FakeVersionFormatter : VersionFormatter {
|
||||||
|
override fun get(): String {
|
||||||
|
return "A Version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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.preferences.impl.root
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
|
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class VersionFormatterTest {
|
||||||
|
@Test
|
||||||
|
fun `version formatter should return simplified version for other branch`() = runTest {
|
||||||
|
val sut = DefaultVersionFormatter(
|
||||||
|
stringProvider = FakeStringProvider(defaultResult = VERSION),
|
||||||
|
buildMeta = aBuildMeta(gitBranchName = "main")
|
||||||
|
)
|
||||||
|
assertThat(sut.get()).isEqualTo(VERSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `version formatter should return simplified version for main branch`() = runTest {
|
||||||
|
val sut = DefaultVersionFormatter(
|
||||||
|
stringProvider = FakeStringProvider(defaultResult = VERSION),
|
||||||
|
buildMeta = aBuildMeta(
|
||||||
|
gitBranchName = "branch",
|
||||||
|
gitRevision = "1234567890",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(sut.get()).isEqualTo("$VERSION\nbranch\n1234567890")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val VERSION = "version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,5 +31,6 @@ interface BugReportEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onBugReportSent()
|
fun onBugReportSent()
|
||||||
|
fun onViewLogs(basePath: String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.rageshake.api.detection
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -73,9 +75,10 @@ private fun TakeScreenshot(
|
||||||
onScreenshotTaken: (ImageResult) -> Unit
|
onScreenshotTaken: (ImageResult) -> Unit
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
val latestOnScreenshotTaken by rememberUpdatedState(onScreenshotTaken)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
view.screenshot {
|
view.screenshot {
|
||||||
onScreenshotTaken(it)
|
latestOnScreenshotTaken(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,4 +52,9 @@ interface BugReporter {
|
||||||
* Set the current tracing filter.
|
* Set the current tracing filter.
|
||||||
*/
|
*/
|
||||||
fun setCurrentTracingFilter(tracingFilter: String)
|
fun setCurrentTracingFilter(tracingFilter: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the logcat.
|
||||||
|
*/
|
||||||
|
fun saveLogCat()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
||||||
|
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||||
import io.element.android.libraries.androidutils.system.toast
|
import io.element.android.libraries.androidutils.system.toast
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -37,7 +38,12 @@ class BugReportNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: BugReportPresenter,
|
private val presenter: BugReportPresenter,
|
||||||
|
private val bugReporter: BugReporter,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
private fun onViewLogs(basePath: String) {
|
||||||
|
plugins<BugReportEntryPoint.Callback>().forEach { it.onViewLogs(basePath) }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
@ -50,6 +56,11 @@ class BugReportNode @AssistedInject constructor(
|
||||||
activity?.toast(CommonStrings.common_report_submitted)
|
activity?.toast(CommonStrings.common_report_submitted)
|
||||||
onDone()
|
onDone()
|
||||||
},
|
},
|
||||||
|
onViewLogs = {
|
||||||
|
// Force a logcat dump
|
||||||
|
bugReporter.saveLogCat()
|
||||||
|
onViewLogs(bugReporter.logDirectory().absolutePath)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,11 @@ import io.element.android.features.rageshake.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||||
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceDivider
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceRow
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceRow
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
|
||||||
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
|
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
|
||||||
|
|
@ -55,6 +57,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@Composable
|
@Composable
|
||||||
fun BugReportView(
|
fun BugReportView(
|
||||||
state: BugReportState,
|
state: BugReportState,
|
||||||
|
onViewLogs: () -> Unit,
|
||||||
onDone: () -> Unit,
|
onDone: () -> Unit,
|
||||||
onBackPressed: () -> Unit,
|
onBackPressed: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -97,6 +100,13 @@ fun BugReportView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
PreferenceDivider()
|
||||||
|
PreferenceText(
|
||||||
|
title = stringResource(id = R.string.screen_bug_report_view_logs),
|
||||||
|
enabled = isFormEnabled,
|
||||||
|
onClick = onViewLogs,
|
||||||
|
)
|
||||||
|
PreferenceDivider()
|
||||||
PreferenceSwitch(
|
PreferenceSwitch(
|
||||||
isChecked = state.formState.sendLogs,
|
isChecked = state.formState.sendLogs,
|
||||||
onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) },
|
onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) },
|
||||||
|
|
@ -169,5 +179,6 @@ internal fun BugReportViewPreview(@PreviewParameter(BugReportStateProvider::clas
|
||||||
state = state,
|
state = state,
|
||||||
onDone = {},
|
onDone = {},
|
||||||
onBackPressed = {},
|
onBackPressed = {},
|
||||||
|
onViewLogs = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,8 @@ class DefaultBugReporter @Inject constructor(
|
||||||
private val logcatCommandDebug = arrayOf("logcat", "-d", "-v", "threadtime", "*:*")
|
private val logcatCommandDebug = arrayOf("logcat", "-d", "-v", "threadtime", "*:*")
|
||||||
private var currentTracingFilter: String? = null
|
private var currentTracingFilter: String? = null
|
||||||
|
|
||||||
|
private val logCatErrFile = File(logDirectory().absolutePath, LOG_CAT_FILENAME)
|
||||||
|
|
||||||
override suspend fun sendBugReport(
|
override suspend fun sendBugReport(
|
||||||
withDevicesLogs: Boolean,
|
withDevicesLogs: Boolean,
|
||||||
withCrashLogs: Boolean,
|
withCrashLogs: Boolean,
|
||||||
|
|
@ -130,8 +132,8 @@ class DefaultBugReporter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isCancelled && (withCrashLogs || withDevicesLogs)) {
|
if (!isCancelled && (withCrashLogs || withDevicesLogs)) {
|
||||||
val gzippedLogcat = saveLogCat()
|
saveLogCat()
|
||||||
|
val gzippedLogcat = compressFile(logCatErrFile)
|
||||||
if (null != gzippedLogcat) {
|
if (null != gzippedLogcat) {
|
||||||
if (gzippedFiles.size == 0) {
|
if (gzippedFiles.size == 0) {
|
||||||
gzippedFiles.add(gzippedLogcat)
|
gzippedFiles.add(gzippedLogcat)
|
||||||
|
|
@ -199,6 +201,9 @@ class DefaultBugReporter @Inject constructor(
|
||||||
|
|
||||||
// add some github labels
|
// add some github labels
|
||||||
builder.addFormDataPart("label", buildMeta.versionName)
|
builder.addFormDataPart("label", buildMeta.versionName)
|
||||||
|
builder.addFormDataPart("label", buildMeta.flavorDescription)
|
||||||
|
builder.addFormDataPart("branch_name", buildMeta.gitBranchName)
|
||||||
|
|
||||||
if (crashCallStack.isNotEmpty() && withCrashLogs) {
|
if (crashCallStack.isNotEmpty() && withCrashLogs) {
|
||||||
builder.addFormDataPart("label", "crash")
|
builder.addFormDataPart("label", "crash")
|
||||||
}
|
}
|
||||||
|
|
@ -320,7 +325,9 @@ class DefaultBugReporter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logDirectory(): File {
|
override fun logDirectory(): File {
|
||||||
return File(context.cacheDir, LOG_DIRECTORY_NAME)
|
return File(context.cacheDir, LOG_DIRECTORY_NAME).apply {
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanLogDirectoryIfNeeded() {
|
override fun cleanLogDirectoryIfNeeded() {
|
||||||
|
|
@ -380,30 +387,19 @@ class DefaultBugReporter @Inject constructor(
|
||||||
*
|
*
|
||||||
* @return the file if the operation succeeds
|
* @return the file if the operation succeeds
|
||||||
*/
|
*/
|
||||||
private fun saveLogCat(): File? {
|
override fun saveLogCat() {
|
||||||
val logCatErrFile = File(context.cacheDir.absolutePath, LOG_CAT_FILENAME)
|
|
||||||
|
|
||||||
if (logCatErrFile.exists()) {
|
if (logCatErrFile.exists()) {
|
||||||
logCatErrFile.safeDelete()
|
logCatErrFile.safeDelete()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logCatErrFile.writer().use {
|
logCatErrFile.writer().use {
|
||||||
getLogCatError(it)
|
getLogCatError(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return compressFile(logCatErrFile)
|
|
||||||
} catch (error: OutOfMemoryError) {
|
} catch (error: OutOfMemoryError) {
|
||||||
Timber.e(error, "## saveLogCat() : fail to write logcat OOM")
|
Timber.e(error, "## saveLogCat() : fail to write logcat OOM")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## saveLogCat() : fail to write logcat")
|
Timber.e(e, "## saveLogCat() : fail to write logcat")
|
||||||
} finally {
|
|
||||||
if (logCatErrFile.exists()) {
|
|
||||||
logCatErrFile.safeDelete()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,6 @@
|
||||||
<string name="screen_bug_report_include_logs">"Povolit protokoly"</string>
|
<string name="screen_bug_report_include_logs">"Povolit protokoly"</string>
|
||||||
<string name="screen_bug_report_include_screenshot">"Odeslat snímek obrazovky"</string>
|
<string name="screen_bug_report_include_screenshot">"Odeslat snímek obrazovky"</string>
|
||||||
<string name="screen_bug_report_logs_description">"Protokoly budou součástí vaší zprávy, aby se zajistilo že vše funguje správně. Chcete-li odeslat zprávu bez protokolů, vypněte toto nastavení."</string>
|
<string name="screen_bug_report_logs_description">"Protokoly budou součástí vaší zprávy, aby se zajistilo že vše funguje správně. Chcete-li odeslat zprávu bez protokolů, vypněte toto nastavení."</string>
|
||||||
|
<string name="screen_bug_report_view_logs">"Zobrazit protokoly"</string>
|
||||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?"</string>
|
<string name="screen_bug_report_rash_logs_alert_title">"%1$s havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,6 @@
|
||||||
<string name="screen_bug_report_include_logs">"Разрешить ведение журналов"</string>
|
<string name="screen_bug_report_include_logs">"Разрешить ведение журналов"</string>
|
||||||
<string name="screen_bug_report_include_screenshot">"Отправить снимок экрана"</string>
|
<string name="screen_bug_report_include_screenshot">"Отправить снимок экрана"</string>
|
||||||
<string name="screen_bug_report_logs_description">"Чтобы убедиться, что все работает правильно, в сообщение будут включены журналы. Чтобы отправить сообщение без журналов, отключите эту настройку."</string>
|
<string name="screen_bug_report_logs_description">"Чтобы убедиться, что все работает правильно, в сообщение будут включены журналы. Чтобы отправить сообщение без журналов, отключите эту настройку."</string>
|
||||||
|
<string name="screen_bug_report_view_logs">"Просмотр журналов"</string>
|
||||||
<string name="screen_bug_report_rash_logs_alert_title">"При последнем использовании %1$s произошел сбой. Хотите поделиться отчетом о сбое?"</string>
|
<string name="screen_bug_report_rash_logs_alert_title">"При последнем использовании %1$s произошел сбой. Хотите поделиться отчетом о сбое?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,6 @@
|
||||||
<string name="screen_bug_report_include_logs">"Povoliť záznamy"</string>
|
<string name="screen_bug_report_include_logs">"Povoliť záznamy"</string>
|
||||||
<string name="screen_bug_report_include_screenshot">"Odoslať snímku obrazovky"</string>
|
<string name="screen_bug_report_include_screenshot">"Odoslať snímku obrazovky"</string>
|
||||||
<string name="screen_bug_report_logs_description">"K vašej správe budú priložené záznamy o chybe, aby sme sa uistili, že všetko funguje správne. Ak chcete odoslať správu bez záznamov o chybe, vypnite toto nastavenie."</string>
|
<string name="screen_bug_report_logs_description">"K vašej správe budú priložené záznamy o chybe, aby sme sa uistili, že všetko funguje správne. Ak chcete odoslať správu bez záznamov o chybe, vypnite toto nastavenie."</string>
|
||||||
|
<string name="screen_bug_report_view_logs">"Zobraziť záznamy"</string>
|
||||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s zlyhal pri poslednom použití. Chcete zdieľať správu o páde s našim tímom?"</string>
|
<string name="screen_bug_report_rash_logs_alert_title">"%1$s zlyhal pri poslednom použití. Chcete zdieľať správu o páde s našim tímom?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,6 @@
|
||||||
<string name="screen_bug_report_include_logs">"Allow logs"</string>
|
<string name="screen_bug_report_include_logs">"Allow logs"</string>
|
||||||
<string name="screen_bug_report_include_screenshot">"Send screenshot"</string>
|
<string name="screen_bug_report_include_screenshot">"Send screenshot"</string>
|
||||||
<string name="screen_bug_report_logs_description">"Logs will be included with your message to make sure that everything is working properly. To send your message without logs, turn off this setting."</string>
|
<string name="screen_bug_report_logs_description">"Logs will be included with your message to make sure that everything is working properly. To send your message without logs, turn off this setting."</string>
|
||||||
|
<string name="screen_bug_report_view_logs">"View logs"</string>
|
||||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s crashed the last time it was used. Would you like to share a crash report with us?"</string>
|
<string name="screen_bug_report_rash_logs_alert_title">"%1$s crashed the last time it was used. Would you like to share a crash report with us?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,10 @@ class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Succes
|
||||||
override fun setCurrentTracingFilter(tracingFilter: String) {
|
override fun setCurrentTracingFilter(tracingFilter: String) {
|
||||||
// No op
|
// No op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun saveLogCat() {
|
||||||
|
// No op
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class FakeBugReporterMode {
|
enum class FakeBugReporterMode {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class DefaultBugReporterTest {
|
||||||
val sut = createDefaultBugReporter(server)
|
val sut = createDefaultBugReporter(server)
|
||||||
var onUploadCancelledCalled = false
|
var onUploadCancelledCalled = false
|
||||||
var onUploadFailedCalled = false
|
var onUploadFailedCalled = false
|
||||||
var progressValues = mutableListOf<Int>()
|
val progressValues = mutableListOf<Int>()
|
||||||
var onUploadSucceedCalled = false
|
var onUploadSucceedCalled = false
|
||||||
sut.sendBugReport(
|
sut.sendBugReport(
|
||||||
withDevicesLogs = true,
|
withDevicesLogs = true,
|
||||||
|
|
@ -80,7 +80,7 @@ class DefaultBugReporterTest {
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
assertThat(onUploadCancelledCalled).isFalse()
|
assertThat(onUploadCancelledCalled).isFalse()
|
||||||
assertThat(onUploadFailedCalled).isFalse()
|
assertThat(onUploadFailedCalled).isFalse()
|
||||||
assertThat(progressValues.size).isEqualTo(10)
|
assertThat(progressValues.size).isEqualTo(EXPECTED_NUMBER_OF_PROGRESS_VALUE)
|
||||||
assertThat(onUploadSucceedCalled).isTrue()
|
assertThat(onUploadSucceedCalled).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,7 +97,7 @@ class DefaultBugReporterTest {
|
||||||
var onUploadCancelledCalled = false
|
var onUploadCancelledCalled = false
|
||||||
var onUploadFailedCalled = false
|
var onUploadFailedCalled = false
|
||||||
var onUploadFailedReason: String? = null
|
var onUploadFailedReason: String? = null
|
||||||
var progressValues = mutableListOf<Int>()
|
val progressValues = mutableListOf<Int>()
|
||||||
var onUploadSucceedCalled = false
|
var onUploadSucceedCalled = false
|
||||||
sut.sendBugReport(
|
sut.sendBugReport(
|
||||||
withDevicesLogs = true,
|
withDevicesLogs = true,
|
||||||
|
|
@ -131,7 +131,7 @@ class DefaultBugReporterTest {
|
||||||
assertThat(onUploadCancelledCalled).isFalse()
|
assertThat(onUploadCancelledCalled).isFalse()
|
||||||
assertThat(onUploadFailedCalled).isTrue()
|
assertThat(onUploadFailedCalled).isTrue()
|
||||||
assertThat(onUploadFailedReason).isEqualTo("An error body")
|
assertThat(onUploadFailedReason).isEqualTo("An error body")
|
||||||
assertThat(progressValues.size).isEqualTo(10)
|
assertThat(progressValues.size).isEqualTo(EXPECTED_NUMBER_OF_PROGRESS_VALUE)
|
||||||
assertThat(onUploadSucceedCalled).isFalse()
|
assertThat(onUploadSucceedCalled).isFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,4 +153,8 @@ class DefaultBugReporterTest {
|
||||||
bugReporterUrlProvider = { server.url("/") }
|
bugReporterUrlProvider = { server.url("/") }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EXPECTED_NUMBER_OF_PROGRESS_VALUE = 12
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,13 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
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.lifecycle.Lifecycle
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
@ -63,6 +65,12 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val leaveRoomState = leaveRoomPresenter.present()
|
val leaveRoomState = leaveRoomPresenter.present()
|
||||||
val canShowNotificationSettings = remember { mutableStateOf(false) }
|
val canShowNotificationSettings = remember { mutableStateOf(false) }
|
||||||
|
val roomInfo = room.roomInfoFlow.collectAsState(initial = null).value
|
||||||
|
|
||||||
|
val roomAvatar by remember { derivedStateOf { roomInfo?.avatarUrl ?: room.avatarUrl } }
|
||||||
|
|
||||||
|
val roomName by remember { derivedStateOf { (roomInfo?.name ?: room.name ?: room.displayName).trim() } }
|
||||||
|
val roomTopic by remember { derivedStateOf { roomInfo?.topic ?: room.topic } }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings)
|
canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings)
|
||||||
|
|
@ -70,7 +78,13 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
room.updateRoomNotificationSettings()
|
room.updateRoomNotificationSettings()
|
||||||
observeNotificationSettings()
|
observeNotificationSettings()
|
||||||
}
|
}
|
||||||
room.updateMembers()
|
}
|
||||||
|
|
||||||
|
// Update room members only when first presenting the node
|
||||||
|
OnLifecycleEvent { _, event ->
|
||||||
|
if (event == Lifecycle.Event.ON_CREATE) {
|
||||||
|
scope.launch { room.updateMembers() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val membersState by room.membersStateFlow.collectAsState()
|
val membersState by room.membersStateFlow.collectAsState()
|
||||||
|
|
@ -82,8 +96,8 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
|
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
|
||||||
val roomType by getRoomType(dmMember)
|
val roomType by getRoomType(dmMember)
|
||||||
|
|
||||||
val topicState = remember(canEditTopic, room.topic, roomType) {
|
val topicState = remember(canEditTopic, roomTopic, roomType) {
|
||||||
val topic = room.topic
|
val topic = roomTopic
|
||||||
|
|
||||||
when {
|
when {
|
||||||
!topic.isNullOrBlank() -> RoomTopicState.ExistingTopic(topic)
|
!topic.isNullOrBlank() -> RoomTopicState.ExistingTopic(topic)
|
||||||
|
|
@ -115,9 +129,9 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
|
|
||||||
return RoomDetailsState(
|
return RoomDetailsState(
|
||||||
roomId = room.roomId.value,
|
roomId = room.roomId.value,
|
||||||
roomName = room.displayName,
|
roomName = roomName,
|
||||||
roomAlias = room.alias,
|
roomAlias = room.alias,
|
||||||
roomAvatarUrl = room.avatarUrl,
|
roomAvatarUrl = roomAvatar,
|
||||||
roomTopic = topicState,
|
roomTopic = topicState,
|
||||||
memberCount = room.joinedMemberCount,
|
memberCount = room.joinedMemberCount,
|
||||||
isEncrypted = room.isEncrypted,
|
isEncrypted = room.isEncrypted,
|
||||||
|
|
|
||||||
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