Merge branch 'release/0.4.2' into main
This commit is contained in:
commit
d78ae8097f
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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.11.1
|
||||
uses: gradle/gradle-build-action@v2.12.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Assemble debug APK
|
||||
|
|
@ -47,14 +47,15 @@ jobs:
|
|||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_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
|
||||
if: ${{ matrix.variant == 'debug' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: elementx-debug
|
||||
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
|
||||
id: diawi
|
||||
# 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 != '' }}
|
||||
with:
|
||||
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.
|
||||
if: ${{ matrix.variant == 'debug' && github.event_name == 'pull_request' && steps.diawi.conclusion == 'success' }}
|
||||
uses: NejcZdovc/comment-pr@v2
|
||||
|
|
@ -82,7 +83,7 @@ jobs:
|
|||
run: ./gradlew compileReleaseSources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Compile nightly sources
|
||||
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
|
||||
if: ${{ matrix.variant == 'samples' }}
|
||||
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.
|
||||
steps:
|
||||
- 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 }}
|
||||
# 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: 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: |
|
||||
USERNAME=maestroelement
|
||||
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
|
||||
- name: Build and upload Nightly application
|
||||
run: |
|
||||
./gradlew assembleNightly appDistributionUploadNightly $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES
|
||||
env:
|
||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||
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
|
||||
continue-on-error: true # don't block anything by this upload failing (for now)
|
||||
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:
|
||||
BROWSERSTACK_USERNAME: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_USERNAME }}
|
||||
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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.11.1
|
||||
uses: gradle/gradle-build-action@v2.12.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- 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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.11.1
|
||||
uses: gradle/gradle-build-action@v2.12.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- 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'
|
||||
# Add gradle cache, this should speed up the process
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.11.1
|
||||
uses: gradle/gradle-build-action@v2.12.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- 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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.11.1
|
||||
uses: gradle/gradle-build-action@v2.12.0
|
||||
- name: Create app bundle
|
||||
env:
|
||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: elementx-app-bundle-unsigned
|
||||
name: elementx-app-gplay-bundle-unsigned
|
||||
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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.11.1
|
||||
uses: gradle/gradle-build-action@v2.12.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- 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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.11.1
|
||||
uses: gradle/gradle-build-action@v2.12.0
|
||||
with:
|
||||
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>swipeable</w>
|
||||
<w>textfields</w>
|
||||
<w>tombstoned</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
|
|
|
|||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.21" />
|
||||
<option name="version" value="1.9.22" />
|
||||
</component>
|
||||
</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)
|
||||
========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
|
|||
import extension.allFeaturesImpl
|
||||
import extension.allLibrariesImpl
|
||||
import extension.allServicesImpl
|
||||
import extension.gitBranchName
|
||||
import extension.gitRevision
|
||||
import extension.koverDependencies
|
||||
import extension.setupKover
|
||||
import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
||||
|
||||
plugins {
|
||||
|
|
@ -36,6 +40,8 @@ plugins {
|
|||
// id("com.google.gms.google-services")
|
||||
}
|
||||
|
||||
setupKover()
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.x"
|
||||
|
||||
|
|
@ -50,6 +56,9 @@ android {
|
|||
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
|
||||
splits {
|
||||
// Configures multiple APKs based on ABI.
|
||||
|
|
@ -69,7 +78,7 @@ android {
|
|||
}
|
||||
|
||||
signingConfigs {
|
||||
named("debug") {
|
||||
getByName("debug") {
|
||||
keyAlias = "androiddebugkey"
|
||||
keyPassword = "android"
|
||||
storeFile = file("./signature/debug.keystore")
|
||||
|
|
@ -87,13 +96,13 @@ android {
|
|||
}
|
||||
|
||||
buildTypes {
|
||||
named("debug") {
|
||||
getByName("debug") {
|
||||
resValue("string", "app_name", "Element X dbg")
|
||||
applicationIdSuffix = ".debug"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
|
||||
named("release") {
|
||||
getByName("release") {
|
||||
resValue("string", "app_name", "Element X")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
|
||||
|
|
@ -124,7 +133,7 @@ android {
|
|||
// We upload the universal APK to fix this error:
|
||||
// "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."
|
||||
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"
|
||||
// artifactPath = "$rootDir/app/build/outputs/bundle/nightly/app-nightly.aab"
|
||||
// This file will be generated by the GitHub action
|
||||
|
|
@ -143,6 +152,20 @@ android {
|
|||
buildFeatures {
|
||||
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 {
|
||||
|
|
@ -222,6 +245,11 @@ dependencies {
|
|||
implementation(projects.appconfig)
|
||||
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.androidx.splash)
|
||||
implementation(libs.androidx.core)
|
||||
|
|
@ -251,4 +279,5 @@ dependencies {
|
|||
testImplementation(projects.libraries.matrix.test)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
koverDependencies()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package io.element.android.x.di
|
|||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
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.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
|
@ -34,5 +34,5 @@ interface AppBindings {
|
|||
|
||||
fun lockScreenService(): LockScreenService
|
||||
|
||||
fun preferencesStore(): PreferencesStore
|
||||
fun preferencesStore(): AppPreferencesStore
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,16 +85,10 @@ object AppModule {
|
|||
lowPrivacyLoggingEnabled = false,
|
||||
versionName = BuildConfig.VERSION_NAME,
|
||||
versionCode = BuildConfig.VERSION_CODE,
|
||||
// BuildConfig.GIT_REVISION,
|
||||
gitRevision = "TODO",
|
||||
// BuildConfig.GIT_REVISION_DATE,
|
||||
gitRevisionDate = "TODO",
|
||||
// BuildConfig.GIT_BRANCH_NAME,
|
||||
gitBranchName = "TODO",
|
||||
// BuildConfig.FLAVOR_DESCRIPTION,
|
||||
flavorDescription = "TODO",
|
||||
// BuildConfig.SHORT_FLAVOR_DESCRIPTION,
|
||||
flavorShortDescription = "TODO",
|
||||
gitRevision = BuildConfig.GIT_REVISION,
|
||||
gitBranchName = BuildConfig.GIT_BRANCH_NAME,
|
||||
flavorDescription = BuildConfig.FLAVOR_DESCRIPTION,
|
||||
flavorShortDescription = BuildConfig.SHORT_FLAVOR_DESCRIPTION,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ interface SessionComponent : NodeFactoriesBindings {
|
|||
interface Builder {
|
||||
@BindsInstance
|
||||
fun client(matrixClient: MatrixClient): Builder
|
||||
|
||||
fun build(): SessionComponent
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ fun logApplicationInfo() {
|
|||
append(BuildConfig.VERSION_CODE)
|
||||
append(") - ")
|
||||
append(BuildConfig.BUILD_TYPE)
|
||||
append(" / ")
|
||||
append(BuildConfig.FLAVOR)
|
||||
}
|
||||
// TODO Get SDK version somehow
|
||||
val sdkVersion = "SDK VERSION (TODO)"
|
||||
|
|
@ -37,6 +39,7 @@ fun logApplicationInfo() {
|
|||
Timber.d("----------------------------------------------------------------")
|
||||
Timber.d("----------------------------------------------------------------")
|
||||
Timber.d(" Application version: $appVersion")
|
||||
Timber.d(" Git SHA: ${BuildConfig.GIT_REVISION}")
|
||||
Timber.d(" SDK version: $sdkVersion")
|
||||
Timber.d(" Local time: $date")
|
||||
Timber.d("----------------------------------------------------------------")
|
||||
|
|
|
|||
|
|
@ -46,8 +46,13 @@ class TracingInitializer : Initializer<Unit> {
|
|||
writesToFilesConfiguration = WriteToFilesConfiguration.Disabled
|
||||
)
|
||||
} else {
|
||||
val config = if (BuildConfig.BUILD_TYPE == "nightly") {
|
||||
TracingFilterConfigurations.nightly
|
||||
} else {
|
||||
TracingFilterConfigurations.release
|
||||
}
|
||||
TracingConfiguration(
|
||||
filterConfiguration = TracingFilterConfigurations.release,
|
||||
filterConfiguration = config,
|
||||
writesToLogcat = false,
|
||||
writesToFilesConfiguration = WriteToFilesConfiguration.Enabled(
|
||||
directory = bugReporter.logDirectory().absolutePath,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ dependencies {
|
|||
implementation(libs.coil)
|
||||
|
||||
implementation(projects.features.ftue.api)
|
||||
implementation(projects.features.viewfolder.api)
|
||||
|
||||
implementation(projects.services.apperror.impl)
|
||||
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.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -54,6 +55,10 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
|||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
interface Callback : Plugin {
|
||||
fun onOpenBugReport()
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
|
|
@ -91,6 +96,10 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
|||
override fun onOpenDeveloperSettings() {
|
||||
backstack.push(NavTarget.ConfigureTracing)
|
||||
}
|
||||
|
||||
override fun onReportProblem() {
|
||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||
}
|
||||
}
|
||||
onBoardingEntryPoint
|
||||
.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.rageshake.api.bugreport.BugReportEntryPoint
|
||||
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.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -70,6 +71,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
private val matrixClientsHolder: MatrixClientsHolder,
|
||||
private val presenter: RootPresenter,
|
||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||
private val viewFolderEntryPoint: ViewFolderEntryPoint,
|
||||
private val signedOutEntryPoint: SignedOutEntryPoint,
|
||||
private val intentResolver: IntentResolver,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
|
|
@ -141,7 +143,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
onSuccess(sessionId)
|
||||
}
|
||||
.onFailure {
|
||||
Timber.v("Failed to restore session $sessionId")
|
||||
Timber.e(it, "Failed to restore session $sessionId")
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
|
|
@ -194,6 +196,11 @@ class RootFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data object BugReport : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class ViewLogs(
|
||||
val rootPath: String,
|
||||
) : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -210,7 +217,14 @@ class RootFlowNode @AssistedInject constructor(
|
|||
}
|
||||
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 -> {
|
||||
signedOutEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(
|
||||
|
|
@ -226,12 +240,31 @@ class RootFlowNode @AssistedInject constructor(
|
|||
override fun onBugReportSent() {
|
||||
backstack.pop()
|
||||
}
|
||||
|
||||
override fun onViewLogs(basePath: String) {
|
||||
backstack.push(NavTarget.ViewLogs(rootPath = basePath))
|
||||
}
|
||||
}
|
||||
bugReportEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
.callback(callback)
|
||||
.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 {
|
||||
val room = inputs.room
|
||||
room.updateMembers()
|
||||
.onFailure {
|
||||
Timber.e(it, "Fail to fetch members for room ${room.roomId}")
|
||||
}
|
||||
.onSuccess {
|
||||
Timber.v("Success fetching members for room ${room.roomId}")
|
||||
}
|
||||
inputs.room.updateMembers()
|
||||
}
|
||||
|
||||
private fun createRoomDetailsNode(buildContext: BuildContext, initialTarget: RoomDetailsEntryPoint.InitialTarget): Node {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ allprojects {
|
|||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.3.9")
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.3.11")
|
||||
}
|
||||
|
||||
// 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.di.CallBindings
|
||||
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 javax.inject.Inject
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
|
|||
|
||||
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
||||
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
||||
@Inject lateinit var preferencesStore: PreferencesStore
|
||||
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
||||
|
||||
private lateinit var presenter: CallScreenPresenter
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
|
|||
|
||||
setContent {
|
||||
val theme by remember {
|
||||
preferencesStore.getThemeFlow().mapToTheme()
|
||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||
}
|
||||
.collectAsState(initial = Theme.System)
|
||||
val state = presenter.present()
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.call.utils
|
|||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
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.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -31,7 +31,7 @@ import javax.inject.Inject
|
|||
@ContributesBinding(AppScope::class)
|
||||
class DefaultCallWidgetProvider @Inject constructor(
|
||||
private val matrixClientsProvider: MatrixClientProvider,
|
||||
private val preferencesStore: PreferencesStore,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
||||
) : CallWidgetProvider {
|
||||
override suspend fun getWidget(
|
||||
|
|
@ -42,7 +42,7 @@ class DefaultCallWidgetProvider @Inject constructor(
|
|||
theme: String?,
|
||||
): Result<Pair<MatrixWidgetDriver, String>> = runCatching {
|
||||
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 callUrl = room.generateWidgetWebViewUrl(widgetSettings, clientId, languageTag, theme).getOrThrow()
|
||||
room.getWidgetDriver(widgetSettings).getOrThrow() to callUrl
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
package io.element.android.features.call.utils
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
||||
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
|
||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
|
|
@ -94,14 +94,14 @@ class DefaultCallWidgetProviderTest {
|
|||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
val preferencesStore = InMemoryPreferencesStore().apply {
|
||||
val preferencesStore = InMemoryAppPreferencesStore().apply {
|
||||
setCustomElementCallBaseUrl("https://custom.element.io")
|
||||
}
|
||||
val settingsProvider = FakeCallWidgetSettingsProvider()
|
||||
val provider = createProvider(
|
||||
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
|
||||
callWidgetSettingsProvider = settingsProvider,
|
||||
preferencesStore = preferencesStore,
|
||||
appPreferencesStore = preferencesStore,
|
||||
)
|
||||
provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme")
|
||||
|
||||
|
|
@ -110,11 +110,11 @@ class DefaultCallWidgetProviderTest {
|
|||
|
||||
private fun createProvider(
|
||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||
preferencesStore: PreferencesStore = InMemoryPreferencesStore(),
|
||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider()
|
||||
) = DefaultCallWidgetProvider(
|
||||
matrixClientProvider,
|
||||
preferencesStore,
|
||||
appPreferencesStore,
|
||||
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.preview.ElementThemedPreview
|
||||
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.CheckableUnresolvedUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableUserRow
|
||||
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.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
|
||||
@Composable
|
||||
|
|
@ -36,23 +37,24 @@ fun SearchMultipleUsersResultItem(
|
|||
onCheckedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (searchResult.isUnresolved) {
|
||||
CheckableUnresolvedUserRow(
|
||||
checked = isUserSelected,
|
||||
modifier = modifier,
|
||||
val data = if (searchResult.isUnresolved) {
|
||||
CheckableUserRowData.Unresolved(
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = searchResult.matrixUser.userId.value,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
} else {
|
||||
CheckableMatrixUserRow(
|
||||
checked = isUserSelected,
|
||||
modifier = modifier,
|
||||
matrixUser = searchResult.matrixUser,
|
||||
avatarSize = AvatarSize.UserListItem,
|
||||
onCheckedChange = onCheckedChange,
|
||||
CheckableUserRowData.Resolved(
|
||||
name = searchResult.matrixUser.getBestName(),
|
||||
subtext = if (searchResult.matrixUser.displayName.isNullOrEmpty()) null else searchResult.matrixUser.userId.value,
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
)
|
||||
}
|
||||
CheckableUserRow(
|
||||
checked = isUserSelected,
|
||||
modifier = modifier,
|
||||
data = data,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.ftue.impl.migration
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.ftue.impl.R
|
||||
|
|
@ -32,8 +34,9 @@ fun MigrationScreenView(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (migrationState.isMigrating.not()) {
|
||||
val latestOnMigrationFinished by rememberUpdatedState(onMigrationFinished)
|
||||
LaunchedEffect(Unit) {
|
||||
onMigrationFinished()
|
||||
latestOnMigrationFinished()
|
||||
}
|
||||
}
|
||||
SunsetPage(
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ class InviteListPresenter @Inject constructor(
|
|||
AvatarData(
|
||||
id = roomId.value,
|
||||
name = name,
|
||||
url = avatarURLString,
|
||||
url = avatarUrl,
|
||||
size = AvatarSize.RoomInviteItem,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
@ -57,8 +59,9 @@ fun InviteListView(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (state.acceptedAction is AsyncData.Success) {
|
||||
val latestOnInviteAccepted by rememberUpdatedState(onInviteAccepted)
|
||||
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.RoomMembershipState
|
||||
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.A_ROOM_ID
|
||||
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.FakeMatrixClient
|
||||
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.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
||||
|
|
@ -425,14 +425,12 @@ class InviteListPresenterTests {
|
|||
postInviteRooms(
|
||||
listOf(
|
||||
RoomSummary.Filled(
|
||||
RoomSummaryDetails(
|
||||
aRoomSummaryDetails(
|
||||
roomId = A_ROOM_ID,
|
||||
name = A_ROOM_NAME,
|
||||
avatarURLString = null,
|
||||
avatarUrl = null,
|
||||
isDirect = false,
|
||||
lastMessage = null,
|
||||
lastMessageTimestamp = null,
|
||||
unreadNotificationCount = 0,
|
||||
inviter = RoomMember(
|
||||
userId = A_USER_ID,
|
||||
displayName = A_USER_NAME,
|
||||
|
|
@ -454,14 +452,12 @@ class InviteListPresenterTests {
|
|||
postInviteRooms(
|
||||
listOf(
|
||||
RoomSummary.Filled(
|
||||
RoomSummaryDetails(
|
||||
aRoomSummaryDetails(
|
||||
roomId = A_ROOM_ID,
|
||||
name = A_ROOM_NAME,
|
||||
avatarURLString = null,
|
||||
avatarUrl = null,
|
||||
isDirect = true,
|
||||
lastMessage = null,
|
||||
lastMessageTimestamp = null,
|
||||
unreadNotificationCount = 0,
|
||||
inviter = RoomMember(
|
||||
userId = A_USER_ID,
|
||||
displayName = A_USER_NAME,
|
||||
|
|
@ -480,14 +476,12 @@ class InviteListPresenterTests {
|
|||
}
|
||||
|
||||
private fun aRoomSummary(id: RoomId = A_ROOM_ID) = RoomSummary.Filled(
|
||||
RoomSummaryDetails(
|
||||
aRoomSummaryDetails(
|
||||
roomId = id,
|
||||
name = A_ROOM_NAME,
|
||||
avatarURLString = null,
|
||||
avatarUrl = null,
|
||||
isDirect = false,
|
||||
lastMessage = null,
|
||||
lastMessageTimestamp = null,
|
||||
unreadNotificationCount = 0,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_private_subtitle">"Вы уверены, что хотите покинуть эту комнату? Эта комната не является публичной, и Вы не сможете присоединиться к ней без приглашения."</string>
|
||||
<string name="leave_room_alert_subtitle">"Вы уверены, что хотите покинуть комнату?"</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.lockscreen.impl.unlock
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
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.DefaultBiometricUnlockCallback
|
||||
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
|
||||
|
|
@ -30,15 +32,16 @@ class PinUnlockHelper @Inject constructor(
|
|||
) {
|
||||
@Composable
|
||||
fun OnUnlockEffect(onUnlock: () -> Unit) {
|
||||
val latestOnUnlock by rememberUpdatedState(onUnlock)
|
||||
DisposableEffect(Unit) {
|
||||
val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() {
|
||||
override fun onBiometricUnlockSuccess() {
|
||||
onUnlock()
|
||||
latestOnUnlock()
|
||||
}
|
||||
}
|
||||
val pinCodeVerifiedCallback = object : DefaultPinCodeManagerCallback() {
|
||||
override fun onPinCodeVerified() {
|
||||
onUnlock()
|
||||
latestOnUnlock()
|
||||
}
|
||||
}
|
||||
biometricUnlockManager.addCallback(biometricUnlockCallback)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.login.impl.changeserver
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
|
||||
|
|
@ -63,8 +65,11 @@ fun ChangeServerView(
|
|||
}
|
||||
}
|
||||
is AsyncData.Loading -> ProgressDialog()
|
||||
is AsyncData.Success -> LaunchedEffect(state.changeServerAction) {
|
||||
onDone()
|
||||
is AsyncData.Success -> {
|
||||
val latestOnDone by rememberUpdatedState(onDone)
|
||||
LaunchedEffect(state.changeServerAction) {
|
||||
latestOnDone()
|
||||
}
|
||||
}
|
||||
AsyncData.Uninitialized -> Unit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,15 +194,17 @@ private fun LoginForm(
|
|||
.onTabOrEnterKeyFocusNext(focusManager)
|
||||
.testTag(TestTags.loginEmailUsername)
|
||||
.autofill(autofillTypes = listOf(AutofillType.Username), onFill = {
|
||||
loginFieldState = it
|
||||
eventSink(LoginPasswordEvents.SetLogin(it))
|
||||
val sanitized = it.sanitize()
|
||||
loginFieldState = sanitized
|
||||
eventSink(LoginPasswordEvents.SetLogin(sanitized))
|
||||
}),
|
||||
placeholder = {
|
||||
Text(text = stringResource(CommonStrings.common_username))
|
||||
},
|
||||
onValueChange = {
|
||||
loginFieldState = it
|
||||
eventSink(LoginPasswordEvents.SetLogin(it))
|
||||
val sanitized = it.sanitize()
|
||||
loginFieldState = sanitized
|
||||
eventSink(LoginPasswordEvents.SetLogin(sanitized))
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Email,
|
||||
|
|
@ -224,7 +226,6 @@ private fun LoginForm(
|
|||
null
|
||||
},
|
||||
)
|
||||
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
if (state.loginAction is AsyncData.Loading) {
|
||||
// Ensure password is hidden when user submits the form
|
||||
|
|
@ -239,12 +240,14 @@ private fun LoginForm(
|
|||
.onTabOrEnterKeyFocusNext(focusManager)
|
||||
.testTag(TestTags.loginPassword)
|
||||
.autofill(autofillTypes = listOf(AutofillType.Password), onFill = {
|
||||
passwordFieldState = it
|
||||
eventSink(LoginPasswordEvents.SetPassword(it))
|
||||
val sanitized = it.sanitize()
|
||||
passwordFieldState = sanitized
|
||||
eventSink(LoginPasswordEvents.SetPassword(sanitized))
|
||||
}),
|
||||
onValueChange = {
|
||||
passwordFieldState = it
|
||||
eventSink(LoginPasswordEvents.SetPassword(it))
|
||||
val sanitized = it.sanitize()
|
||||
passwordFieldState = sanitized
|
||||
eventSink(LoginPasswordEvents.SetPassword(sanitized))
|
||||
},
|
||||
placeholder = {
|
||||
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
|
||||
private fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
|
||||
ErrorDialog(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.logout.impl.ui
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.logout.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
|
@ -52,9 +54,11 @@ fun LogoutActionDialog(
|
|||
onRetry = onForceLogoutClicked,
|
||||
onDismiss = onDismissError,
|
||||
)
|
||||
is AsyncAction.Success ->
|
||||
is AsyncAction.Success -> {
|
||||
val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout)
|
||||
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.networkmonitor.api.NetworkMonitor
|
||||
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.architecture.AsyncData
|
||||
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.MatrixRoomMembersState
|
||||
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.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.textcomposer.model.MessageComposerMode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -107,12 +107,11 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
private val messageSummaryFormatter: MessageSummaryFormatter,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val preferencesStore: PreferencesStore,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val featureFlagsService: FeatureFlagService,
|
||||
private val htmlConverterProvider: HtmlConverterProvider,
|
||||
@Assisted private val navigator: MessagesNavigator,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val currentSessionIdHolder: CurrentSessionIdHolder,
|
||||
) : Presenter<MessagesState> {
|
||||
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
|
||||
|
||||
|
|
@ -123,7 +122,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun present(): MessagesState {
|
||||
htmlConverterProvider.Update(currentUserId = currentSessionIdHolder.current)
|
||||
htmlConverterProvider.Update(currentUserId = room.sessionId)
|
||||
|
||||
val roomInfo by room.roomInfoFlow.collectAsState(null)
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
|
@ -138,7 +137,8 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
|
||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||
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 roomName: AsyncData<String> by remember {
|
||||
derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
||||
|
|
@ -155,15 +155,15 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
LaunchedEffect(currentSessionIdHolder.current) {
|
||||
LaunchedEffect(syncUpdateFlow.value) {
|
||||
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) }
|
||||
var showReinvitePrompt by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) {
|
||||
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow.value) {
|
||||
withContext(dispatchers.io) {
|
||||
showReinvitePrompt = !hasDismissedInviteDialog && composerState.hasFocus && room.isDirect && room.activeMemberCount == 1L
|
||||
}
|
||||
|
|
@ -176,7 +176,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
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) }
|
||||
LaunchedEffect(featureFlagsService) {
|
||||
|
|
@ -219,7 +219,8 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
roomName = roomName,
|
||||
roomAvatar = roomAvatar,
|
||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||
userHasPermissionToRedact = userHasPermissionToRedact,
|
||||
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
|
||||
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
|
||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
||||
composerState = composerState,
|
||||
voiceMessageComposerState = voiceMessageComposerState,
|
||||
|
|
@ -312,7 +313,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun handleActionEdit(
|
||||
private fun handleActionEdit(
|
||||
targetEvent: TimelineItem.Event,
|
||||
composerState: MessageComposerState,
|
||||
enableTextFormatting: Boolean,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ data class MessagesState(
|
|||
val roomName: AsyncData<String>,
|
||||
val roomAvatar: AsyncData<AvatarData>,
|
||||
val userHasPermissionToSendMessage: Boolean,
|
||||
val userHasPermissionToRedact: Boolean,
|
||||
val userHasPermissionToRedactOwn: Boolean,
|
||||
val userHasPermissionToRedactOther: Boolean,
|
||||
val userHasPermissionToSendReaction: Boolean,
|
||||
val composerState: MessageComposerState,
|
||||
val voiceMessageComposerState: VoiceMessageComposerState,
|
||||
|
|
|
|||
|
|
@ -86,7 +86,8 @@ fun aMessagesState() = MessagesState(
|
|||
roomName = AsyncData.Success("Room name"),
|
||||
roomAvatar = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
|
||||
userHasPermissionToSendMessage = true,
|
||||
userHasPermissionToRedact = false,
|
||||
userHasPermissionToRedactOwn = false,
|
||||
userHasPermissionToRedactOther = false,
|
||||
userHasPermissionToSendReaction = true,
|
||||
composerState = aMessageComposerState().copy(
|
||||
richTextEditorState = RichTextEditorState("Hello", initialFocus = true),
|
||||
|
|
|
|||
|
|
@ -41,8 +41,10 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
|
|
@ -160,7 +162,8 @@ fun MessagesView(
|
|||
state.actionListState.eventSink(
|
||||
ActionListEvents.ComputeForMessage(
|
||||
event = event,
|
||||
canRedact = state.userHasPermissionToRedact,
|
||||
canRedactOwn = state.userHasPermissionToRedactOwn,
|
||||
canRedactOther = state.userHasPermissionToRedactOther,
|
||||
canSendMessage = state.userHasPermissionToSendMessage,
|
||||
canSendReaction = state.userHasPermissionToSendReaction,
|
||||
)
|
||||
|
|
@ -293,8 +296,11 @@ private fun AttachmentStateView(
|
|||
) {
|
||||
when (state) {
|
||||
AttachmentsState.None -> Unit
|
||||
is AttachmentsState.Previewing -> LaunchedEffect(state) {
|
||||
onPreviewAttachments(state.attachments)
|
||||
is AttachmentsState.Previewing -> {
|
||||
val latestOnPreviewAttachments by rememberUpdatedState(onPreviewAttachments)
|
||||
LaunchedEffect(state) {
|
||||
latestOnPreviewAttachments(state.attachments)
|
||||
}
|
||||
}
|
||||
is AttachmentsState.Sending -> {
|
||||
ProgressDialog(
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ sealed interface ActionListEvents {
|
|||
data object Clear : ActionListEvents
|
||||
data class ComputeForMessage(
|
||||
val event: TimelineItem.Event,
|
||||
val canRedact: Boolean,
|
||||
val canRedactOwn: Boolean,
|
||||
val canRedactOther: Boolean,
|
||||
val canSendMessage: Boolean,
|
||||
val canSendReaction: Boolean,
|
||||
) : 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.canBeCopied
|
||||
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 kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -39,7 +39,7 @@ import kotlinx.coroutines.launch
|
|||
import javax.inject.Inject
|
||||
|
||||
class ActionListPresenter @Inject constructor(
|
||||
private val preferencesStore: PreferencesStore,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : Presenter<ActionListState> {
|
||||
@Composable
|
||||
override fun present(): ActionListState {
|
||||
|
|
@ -49,14 +49,15 @@ class ActionListPresenter @Inject constructor(
|
|||
mutableStateOf(ActionListState.Target.None)
|
||||
}
|
||||
|
||||
val isDeveloperModeEnabled by preferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
|
||||
val isDeveloperModeEnabled by appPreferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
|
||||
|
||||
fun handleEvents(event: ActionListEvents) {
|
||||
when (event) {
|
||||
ActionListEvents.Clear -> target.value = ActionListState.Target.None
|
||||
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
|
||||
timelineItem = event.event,
|
||||
userCanRedact = event.canRedact,
|
||||
userCanRedactOwn = event.canRedactOwn,
|
||||
userCanRedactOther = event.canRedactOther,
|
||||
userCanSendMessage = event.canSendMessage,
|
||||
userCanSendReaction = event.canSendReaction,
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
|
|
@ -73,13 +74,15 @@ class ActionListPresenter @Inject constructor(
|
|||
|
||||
private fun CoroutineScope.computeForMessage(
|
||||
timelineItem: TimelineItem.Event,
|
||||
userCanRedact: Boolean,
|
||||
userCanRedactOwn: Boolean,
|
||||
userCanRedactOther: Boolean,
|
||||
userCanSendMessage: Boolean,
|
||||
userCanSendReaction: Boolean,
|
||||
isDeveloperModeEnabled: Boolean,
|
||||
target: MutableState<ActionListState.Target>
|
||||
) = launch {
|
||||
target.value = ActionListState.Target.Loading(timelineItem)
|
||||
val canRedact = timelineItem.isMine && userCanRedactOwn || !timelineItem.isMine && userCanRedactOther
|
||||
val actions =
|
||||
when (timelineItem.content) {
|
||||
is TimelineItemRedactedContent -> {
|
||||
|
|
@ -98,8 +101,10 @@ class ActionListPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
is TimelineItemPollContent -> {
|
||||
val canEndPoll = timelineItem.isRemote &&
|
||||
!timelineItem.content.isEnded &&
|
||||
(timelineItem.isMine || canRedact)
|
||||
buildList {
|
||||
val isMineOrCanRedact = timelineItem.isMine || userCanRedact
|
||||
if (timelineItem.isRemote) {
|
||||
// Can only reply or forward messages already uploaded to the server
|
||||
add(TimelineItemAction.Reply)
|
||||
|
|
@ -107,7 +112,7 @@ class ActionListPresenter @Inject constructor(
|
|||
if (timelineItem.isRemote && timelineItem.isEditable) {
|
||||
add(TimelineItemAction.Edit)
|
||||
}
|
||||
if (timelineItem.isRemote && !timelineItem.content.isEnded && isMineOrCanRedact) {
|
||||
if (canEndPoll) {
|
||||
add(TimelineItemAction.EndPoll)
|
||||
}
|
||||
if (timelineItem.content.canBeCopied()) {
|
||||
|
|
@ -119,7 +124,7 @@ class ActionListPresenter @Inject constructor(
|
|||
if (!timelineItem.isMine) {
|
||||
add(TimelineItemAction.ReportContent)
|
||||
}
|
||||
if (isMineOrCanRedact) {
|
||||
if (canRedact) {
|
||||
add(TimelineItemAction.Redact)
|
||||
}
|
||||
}
|
||||
|
|
@ -136,7 +141,7 @@ class ActionListPresenter @Inject constructor(
|
|||
if (!timelineItem.isMine) {
|
||||
add(TimelineItemAction.ReportContent)
|
||||
}
|
||||
if (timelineItem.isMine || userCanRedact) {
|
||||
if (canRedact) {
|
||||
add(TimelineItemAction.Redact)
|
||||
}
|
||||
}
|
||||
|
|
@ -169,7 +174,7 @@ class ActionListPresenter @Inject constructor(
|
|||
if (!timelineItem.isMine) {
|
||||
add(TimelineItemAction.ReportContent)
|
||||
}
|
||||
if (timelineItem.isMine || userCanRedact) {
|
||||
if (canRedact) {
|
||||
add(TimelineItemAction.Redact)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -57,8 +59,9 @@ fun AttachmentsPreviewView(
|
|||
}
|
||||
|
||||
if (state.sendActionState is SendActionState.Done) {
|
||||
val latestOnDismiss by rememberUpdatedState(onDismiss)
|
||||
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 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.persistentListOf
|
||||
|
||||
|
|
@ -51,30 +48,3 @@ fun aForwardMessagesState(
|
|||
forwardingSucceeded = forwardingSucceeded,
|
||||
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 object CancelSendAttachment : MessageComposerEvents
|
||||
data class Error(val error: Throwable) : MessageComposerEvents
|
||||
data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents
|
||||
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
|
||||
data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import android.Manifest
|
|||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -207,6 +208,15 @@ class MessageComposerPresenter @Inject constructor(
|
|||
.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) {
|
||||
when (event) {
|
||||
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
|
||||
|
|
@ -299,6 +309,11 @@ class MessageComposerPresenter @Inject constructor(
|
|||
is MessageComposerEvents.Error -> {
|
||||
analyticsService.trackError(event.error)
|
||||
}
|
||||
is MessageComposerEvents.TypingNotice -> {
|
||||
localCoroutineScope.launch {
|
||||
room.typingNotice(event.isTyping)
|
||||
}
|
||||
}
|
||||
is MessageComposerEvents.SuggestionReceived -> {
|
||||
suggestionSearchTrigger.value = event.suggestion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ internal fun MessageComposerView(
|
|||
state.eventSink(MessageComposerEvents.Error(error))
|
||||
}
|
||||
|
||||
fun onTyping(typing: Boolean) {
|
||||
state.eventSink(MessageComposerEvents.TypingNotice(typing))
|
||||
}
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
fun onRequestFocus() {
|
||||
coroutineScope.launch {
|
||||
|
|
@ -121,6 +125,7 @@ internal fun MessageComposerView(
|
|||
onDeleteVoiceMessage = onDeleteVoiceMessage,
|
||||
onSuggestionReceived = ::onSuggestionReceived,
|
||||
onError = ::onError,
|
||||
onTyping = ::onTyping,
|
||||
currentUserId = state.currentUserId,
|
||||
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.poll.api.actions.EndPollAction
|
||||
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.core.coroutine.CoroutineDispatchers
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -72,6 +74,7 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
||||
private val sendPollResponseAction: SendPollResponseAction,
|
||||
private val endPollAction: EndPollAction,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
) : Presenter<TimelineState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
@ -102,6 +105,8 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState()
|
||||
val keyBackupState by encryptionService.backupStateStateFlow.collectAsState()
|
||||
|
||||
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
||||
|
||||
val sessionState by remember {
|
||||
derivedStateOf {
|
||||
SessionState(
|
||||
|
|
@ -111,8 +116,6 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val membersState by room.membersStateFlow.collectAsState()
|
||||
|
||||
fun handleEvents(event: TimelineEvents) {
|
||||
when (event) {
|
||||
TimelineEvents.LoadMore -> localScope.paginateBackwards()
|
||||
|
|
@ -125,7 +128,8 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
firstVisibleIndex = event.firstIndex,
|
||||
timelineItems = timelineItems,
|
||||
lastReadReceiptIndex = lastReadReceiptIndex,
|
||||
lastReadReceiptId = lastReadReceiptId
|
||||
lastReadReceiptId = lastReadReceiptId,
|
||||
readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE,
|
||||
)
|
||||
}
|
||||
is TimelineEvents.PollAnswerSelected -> appScope.launch {
|
||||
|
|
@ -149,13 +153,12 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
timeline
|
||||
.timelineItems
|
||||
.onEach {
|
||||
combine(timeline.timelineItems, room.membersStateFlow) { items, membersState ->
|
||||
timelineItemsFactory.replaceWith(
|
||||
timelineItems = it,
|
||||
timelineItems = items,
|
||||
roomMembers = membersState.roomMembers().orEmpty()
|
||||
)
|
||||
items
|
||||
}
|
||||
.onEach { timelineItems ->
|
||||
if (timelineItems.isEmpty()) {
|
||||
|
|
@ -225,13 +228,14 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
timelineItems: ImmutableList<TimelineItem>,
|
||||
lastReadReceiptIndex: MutableState<Int>,
|
||||
lastReadReceiptId: MutableState<EventId?>,
|
||||
readReceiptType: ReceiptType,
|
||||
) = launch(dispatchers.computation) {
|
||||
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
|
||||
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
|
||||
if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) {
|
||||
lastReadReceiptIndex.value = firstVisibleIndex
|
||||
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.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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") {
|
||||
TimelineItemRoomBeginningView(roomName = roomName)
|
||||
}
|
||||
|
|
@ -193,10 +194,11 @@ private fun BoxScope.TimelineScrollHelper(
|
|||
}
|
||||
}
|
||||
|
||||
val latestOnScrollFinishedAt by rememberUpdatedState(onScrollFinishedAt)
|
||||
LaunchedEffect(isScrollFinished, isTimelineEmpty) {
|
||||
if (isScrollFinished && !isTimelineEmpty) {
|
||||
// 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 androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
|
|
@ -59,23 +60,27 @@ fun ContentAvoidingLayout(
|
|||
) {
|
||||
val scope = remember { ContentAvoidingLayoutScopeInstance() }
|
||||
|
||||
SubcomposeLayout(
|
||||
Layout(
|
||||
modifier = modifier,
|
||||
) { constraints ->
|
||||
content = {
|
||||
scope.content()
|
||||
overlay()
|
||||
}
|
||||
) { measurables, constraints ->
|
||||
|
||||
// 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) {
|
||||
Constraints(minWidth = 0, maxWidth = constraints.maxWidth - overlayPlaceable.width)
|
||||
} else {
|
||||
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 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
|
||||
val freeSpace = max(contentPlaceable.width - data.nonOverlappingContentWidth, 0)
|
||||
|
|
@ -135,13 +140,10 @@ interface ContentAvoidingLayoutScope {
|
|||
}
|
||||
|
||||
private class ContentAvoidingLayoutScopeInstance(
|
||||
val data: ContentAvoidingLayoutData = ContentAvoidingLayoutData(),
|
||||
val data: MutableState<ContentAvoidingLayoutData> = mutableStateOf(ContentAvoidingLayoutData()),
|
||||
) : ContentAvoidingLayoutScope {
|
||||
override fun onContentLayoutChanged(data: ContentAvoidingLayoutData) {
|
||||
this.data.contentWidth = data.contentWidth
|
||||
this.data.contentHeight = data.contentHeight
|
||||
this.data.nonOverlappingContentWidth = data.nonOverlappingContentWidth
|
||||
this.data.nonOverlappingContentHeight = data.nonOverlappingContentHeight
|
||||
this.data.value = data
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,16 @@ class TimelineItemsFactory @Inject constructor(
|
|||
newTimelineItemStates.add(timelineItemState)
|
||||
}
|
||||
} 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()
|
||||
|
|
|
|||
|
|
@ -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.PollContent
|
||||
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.RoomMembershipContent
|
||||
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.UnableToDecryptContent
|
||||
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
|
||||
|
||||
class TimelineItemContentFactory @Inject constructor(
|
||||
|
|
@ -50,7 +50,7 @@ class TimelineItemContentFactory @Inject constructor(
|
|||
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
|
||||
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
|
||||
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)
|
||||
}
|
||||
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.timeline.MatrixTimelineItem
|
||||
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 java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
|
@ -52,21 +53,7 @@ class TimelineItemEventFactory @Inject constructor(
|
|||
val currentSender = currentTimelineItem.event.sender
|
||||
val groupPosition =
|
||||
computeGroupPosition(currentTimelineItem, timelineItems, index)
|
||||
val senderDisplayName: String?
|
||||
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 (senderDisplayName, senderAvatarUrl) = currentTimelineItem.getSenderInfo()
|
||||
|
||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
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 {
|
||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
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_video">"Enregistrer une vidéo"</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_poll">"Sondage"</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.featureflag.api.FeatureFlags
|
||||
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.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
|
|
@ -120,7 +121,7 @@ class MessagesPresenterTest {
|
|||
assertThat(initialState.roomAvatar)
|
||||
.isEqualTo(AsyncData.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom)))
|
||||
assertThat(initialState.userHasPermissionToSendMessage).isTrue()
|
||||
assertThat(initialState.userHasPermissionToRedact).isFalse()
|
||||
assertThat(initialState.userHasPermissionToRedactOwn).isFalse()
|
||||
assertThat(initialState.hasNetworkConnection).isTrue()
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized)
|
||||
|
|
@ -601,14 +602,29 @@ class MessagesPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - permission to redact`() = runTest {
|
||||
val matrixRoom = FakeMatrixRoom(canRedact = true)
|
||||
fun `present - permission to redact own`() = runTest {
|
||||
val matrixRoom = FakeMatrixRoom(canRedactOwn = true)
|
||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedact }.last()
|
||||
assertThat(initialState.userHasPermissionToRedact).isTrue()
|
||||
val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedactOwn }.last()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -649,10 +665,11 @@ class MessagesPresenterTest {
|
|||
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
||||
currentSessionIdHolder: CurrentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
|
||||
): MessagesPresenter {
|
||||
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
|
||||
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
|
||||
val appPreferencesStore = InMemoryAppPreferencesStore(isRichTextEditorEnabled = true)
|
||||
val sessionPreferencesStore = InMemorySessionPreferencesStore()
|
||||
val messageComposerPresenter = MessageComposerPresenter(
|
||||
appCoroutineScope = this,
|
||||
room = matrixRoom,
|
||||
|
|
@ -687,14 +704,14 @@ class MessagesPresenterTest {
|
|||
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||
endPollAction = FakeEndPollAction(),
|
||||
sendPollResponseAction = FakeSendPollResponseAction(),
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
||||
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
||||
return timelinePresenter
|
||||
}
|
||||
}
|
||||
val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true)
|
||||
val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore)
|
||||
val actionListPresenter = ActionListPresenter(appPreferencesStore = appPreferencesStore)
|
||||
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
||||
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
||||
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
||||
|
|
@ -714,11 +731,10 @@ class MessagesPresenterTest {
|
|||
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
||||
navigator = navigator,
|
||||
clipboardHelper = clipboardHelper,
|
||||
preferencesStore = preferencesStore,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
featureFlagsService = FakeFeatureFlagService(),
|
||||
buildMeta = aBuildMeta(),
|
||||
dispatchers = coroutineDispatchers,
|
||||
currentSessionIdHolder = currentSessionIdHolder,
|
||||
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.aTimelineItemVoiceContent
|
||||
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.tests.testutils.WarmUpRule
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -38,6 +38,7 @@ import kotlinx.coroutines.test.runTest
|
|||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class ActionListPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
|
@ -61,7 +62,15 @@ class ActionListPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
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()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -87,7 +96,15 @@ class ActionListPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
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()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -116,7 +133,15 @@ class ActionListPresenterTest {
|
|||
isMine = false,
|
||||
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()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -149,7 +174,15 @@ class ActionListPresenterTest {
|
|||
isMine = false,
|
||||
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()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -181,7 +214,15 @@ class ActionListPresenterTest {
|
|||
isMine = false,
|
||||
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()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
|
|
@ -213,7 +254,15 @@ class ActionListPresenterTest {
|
|||
isMine = false,
|
||||
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()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
|
|
@ -245,7 +294,15 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
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()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
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
|
||||
fun `present - compute for a media item`() = runTest {
|
||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
||||
|
|
@ -279,7 +377,15 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
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()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -311,7 +417,15 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
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()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -341,7 +455,15 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
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()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -370,7 +492,15 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
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()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
|
|
@ -408,10 +538,26 @@ class ActionListPresenterTest {
|
|||
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)
|
||||
|
||||
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 {
|
||||
assertThat(target).isEqualTo(ActionListState.Target.None)
|
||||
}
|
||||
|
|
@ -432,7 +578,15 @@ class ActionListPresenterTest {
|
|||
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()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
|
|
@ -460,7 +614,15 @@ class ActionListPresenterTest {
|
|||
isEditable = true,
|
||||
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()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
|
|
@ -489,7 +651,15 @@ class ActionListPresenterTest {
|
|||
isEditable = false,
|
||||
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()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
|
|
@ -517,7 +687,15 @@ class ActionListPresenterTest {
|
|||
isEditable = false,
|
||||
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()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
|
|
@ -543,7 +721,15 @@ class ActionListPresenterTest {
|
|||
isMine = true,
|
||||
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()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
|
|
@ -561,6 +747,6 @@ class ActionListPresenterTest {
|
|||
}
|
||||
|
||||
private fun createActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter {
|
||||
val preferencesStore = InMemoryPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
|
||||
return ActionListPresenter(preferencesStore = preferencesStore)
|
||||
val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
|
||||
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.test.AN_EVENT_ID
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -54,7 +54,7 @@ class ForwardMessagesPresenterTests {
|
|||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val summary = aRoomSummaryDetail()
|
||||
val summary = aRoomSummaryDetails()
|
||||
presenter.onRoomSelected(listOf(summary.roomId))
|
||||
val forwardingState = awaitItem()
|
||||
assertThat(forwardingState.isForwarding).isTrue()
|
||||
|
|
@ -74,7 +74,7 @@ class ForwardMessagesPresenterTests {
|
|||
// Test failed forwarding
|
||||
room.givenForwardEventResult(Result.failure(Throwable("error")))
|
||||
skipItems(1)
|
||||
val summary = aRoomSummaryDetail()
|
||||
val summary = aRoomSummaryDetails()
|
||||
presenter.onRoomSelected(listOf(summary.roomId))
|
||||
skipItems(1)
|
||||
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 {
|
||||
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
|
||||
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.FakeSendPollResponseAction
|
||||
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.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.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.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.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.aMessageContent
|
||||
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.Test
|
||||
import java.util.Date
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
||||
|
||||
|
|
@ -129,13 +136,41 @@ class TimelinePresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||
val initialState = awaitFirstItem()
|
||||
awaitWithLatch { latch ->
|
||||
timeline.sendReadReceiptLatch = latch
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -151,13 +186,13 @@ class TimelinePresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||
val initialState = awaitFirstItem()
|
||||
awaitWithLatch { latch ->
|
||||
timeline.sendReadReceiptLatch = latch
|
||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||
}
|
||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
|
@ -173,13 +208,13 @@ class TimelinePresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||
val initialState = awaitFirstItem()
|
||||
awaitWithLatch { latch ->
|
||||
timeline.sendReadReceiptLatch = latch
|
||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
||||
}
|
||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||
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 {
|
||||
// Skip 1 item if Mentions feature is enabled
|
||||
if (FeatureFlags.Mentions.defaultValue) {
|
||||
|
|
@ -363,15 +442,17 @@ class TimelinePresenterTest {
|
|||
|
||||
private fun TestScope.createTimelinePresenter(
|
||||
timeline: MatrixTimeline = FakeMatrixTimeline(),
|
||||
room: FakeMatrixRoom = FakeMatrixRoom(matrixTimeline = timeline),
|
||||
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(),
|
||||
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
||||
endPollAction: EndPollAction = FakeEndPollAction(),
|
||||
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
||||
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
): TimelinePresenter {
|
||||
return TimelinePresenter(
|
||||
timelineItemsFactory = timelineItemsFactory,
|
||||
room = FakeMatrixRoom(matrixTimeline = timeline),
|
||||
room = room,
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
appScope = this,
|
||||
navigator = messagesNavigator,
|
||||
|
|
@ -380,6 +461,7 @@ class TimelinePresenterTest {
|
|||
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
||||
endPollAction = endPollAction,
|
||||
sendPollResponseAction = sendPollResponseAction,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,5 +33,6 @@ interface OnBoardingEntryPoint : FeatureEntryPoint {
|
|||
fun onSignUp()
|
||||
fun onSignIn()
|
||||
fun onOpenDeveloperSettings()
|
||||
fun onReportProblem()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ class OnBoardingNode @AssistedInject constructor(
|
|||
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onOpenDeveloperSettings() }
|
||||
}
|
||||
|
||||
private fun onReportProblem() {
|
||||
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onReportProblem() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
|
@ -59,6 +63,7 @@ class OnBoardingNode @AssistedInject constructor(
|
|||
onCreateAccount = ::onSignUp,
|
||||
onSignInWithQrCode = { /* Not supported yet */ },
|
||||
onOpenDeveloperSettings = ::onOpenDeveloperSettings,
|
||||
onReportProblem = ::onReportProblem,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.onboarding.impl
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
|
@ -65,6 +66,7 @@ fun OnBoardingView(
|
|||
onSignIn: () -> Unit,
|
||||
onCreateAccount: () -> Unit,
|
||||
onOpenDeveloperSettings: () -> Unit,
|
||||
onReportProblem: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
OnBoardingPage(
|
||||
|
|
@ -81,6 +83,7 @@ fun OnBoardingView(
|
|||
onSignInWithQrCode = onSignInWithQrCode,
|
||||
onSignIn = onSignIn,
|
||||
onCreateAccount = onCreateAccount,
|
||||
onReportProblem = onReportProblem,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -154,6 +157,7 @@ private fun OnBoardingButtons(
|
|||
onSignInWithQrCode: () -> Unit,
|
||||
onSignIn: () -> Unit,
|
||||
onCreateAccount: () -> Unit,
|
||||
onReportProblem: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ButtonColumnMolecule(modifier = modifier) {
|
||||
|
|
@ -187,6 +191,15 @@ private fun OnBoardingButtons(
|
|||
)
|
||||
}
|
||||
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 = {},
|
||||
onSignIn = {},
|
||||
onCreateAccount = {},
|
||||
onOpenDeveloperSettings = {}
|
||||
onOpenDeveloperSettings = {},
|
||||
onReportProblem = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ dependencies {
|
|||
testImplementation(projects.libraries.indicator.impl)
|
||||
testImplementation(projects.features.logout.impl)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.features.analytics.impl)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import io.element.android.compound.theme.Theme
|
|||
sealed interface AdvancedSettingsEvents {
|
||||
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data class SetSendPublicReadReceiptsEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data object ChangeTheme : AdvancedSettingsEvents
|
||||
data object CancelChangeTheme : AdvancedSettingsEvents
|
||||
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
||||
|
|
|
|||
|
|
@ -25,40 +25,48 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.compound.theme.Theme
|
||||
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 kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class AdvancedSettingsPresenter @Inject constructor(
|
||||
private val preferencesStore: PreferencesStore,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
) : Presenter<AdvancedSettingsState> {
|
||||
@Composable
|
||||
override fun present(): AdvancedSettingsState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val isRichTextEditorEnabled by preferencesStore
|
||||
val isRichTextEditorEnabled by appPreferencesStore
|
||||
.isRichTextEditorEnabledFlow()
|
||||
.collectAsState(initial = false)
|
||||
val isDeveloperModeEnabled by preferencesStore
|
||||
val isDeveloperModeEnabled by appPreferencesStore
|
||||
.isDeveloperModeEnabledFlow()
|
||||
.collectAsState(initial = false)
|
||||
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore
|
||||
.isSendPublicReadReceiptsEnabled()
|
||||
.collectAsState(initial = true)
|
||||
val theme by remember {
|
||||
preferencesStore.getThemeFlow().mapToTheme()
|
||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||
}
|
||||
.collectAsState(initial = Theme.System)
|
||||
var showChangeThemeDialog by remember { mutableStateOf(false) }
|
||||
fun handleEvents(event: AdvancedSettingsEvents) {
|
||||
when (event) {
|
||||
is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch {
|
||||
preferencesStore.setRichTextEditorEnabled(event.enabled)
|
||||
appPreferencesStore.setRichTextEditorEnabled(event.enabled)
|
||||
}
|
||||
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.ChangeTheme -> showChangeThemeDialog = true
|
||||
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
|
||||
preferencesStore.setTheme(event.theme.name)
|
||||
appPreferencesStore.setTheme(event.theme.name)
|
||||
showChangeThemeDialog = false
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +75,7 @@ class AdvancedSettingsPresenter @Inject constructor(
|
|||
return AdvancedSettingsState(
|
||||
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
|
||||
theme = theme,
|
||||
showChangeThemeDialog = showChangeThemeDialog,
|
||||
eventSink = { handleEvents(it) }
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import io.element.android.compound.theme.Theme
|
|||
data class AdvancedSettingsState(
|
||||
val isRichTextEditorEnabled: Boolean,
|
||||
val isDeveloperModeEnabled: Boolean,
|
||||
val isSendPublicReadReceiptsEnabled: Boolean,
|
||||
val theme: Theme,
|
||||
val showChangeThemeDialog: Boolean,
|
||||
val eventSink: (AdvancedSettingsEvents) -> Unit
|
||||
|
|
|
|||
|
|
@ -26,16 +26,19 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
|
|||
aAdvancedSettingsState(isRichTextEditorEnabled = true),
|
||||
aAdvancedSettingsState(isDeveloperModeEnabled = true),
|
||||
aAdvancedSettingsState(showChangeThemeDialog = true),
|
||||
aAdvancedSettingsState(isSendPublicReadReceiptsEnabled = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun aAdvancedSettingsState(
|
||||
isRichTextEditorEnabled: Boolean = false,
|
||||
isDeveloperModeEnabled: Boolean = false,
|
||||
isSendPublicReadReceiptsEnabled: Boolean = false,
|
||||
showChangeThemeDialog: Boolean = false,
|
||||
) = AdvancedSettingsState(
|
||||
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
|
||||
theme = Theme.System,
|
||||
showChangeThemeDialog = showChangeThemeDialog,
|
||||
eventSink = {}
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ fun AdvancedSettingsView(
|
|||
},
|
||||
trailingContent = ListItemContent.Switch(
|
||||
checked = state.isRichTextEditorEnabled,
|
||||
onChange = { state.eventSink(AdvancedSettingsEvents.SetRichTextEditorEnabled(it)) },
|
||||
),
|
||||
onClick = { state.eventSink(AdvancedSettingsEvents.SetRichTextEditorEnabled(!state.isRichTextEditorEnabled)) }
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
|
|
@ -78,8 +78,20 @@ fun AdvancedSettingsView(
|
|||
},
|
||||
trailingContent = ListItemContent.Switch(
|
||||
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.snapshots.SnapshotStateMap
|
||||
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.ComputeCacheSizeUseCase
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
|
||||
|
|
@ -51,7 +51,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
|
||||
private val clearCacheUseCase: ClearCacheUseCase,
|
||||
private val rageshakePresenter: RageshakePreferencesPresenter,
|
||||
private val preferencesStore: PreferencesStore,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : Presenter<DeveloperSettingsState> {
|
||||
@Composable
|
||||
override fun present(): DeveloperSettingsState {
|
||||
|
|
@ -69,7 +69,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
val clearCacheAction = remember {
|
||||
mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
||||
}
|
||||
val customElementCallBaseUrl by preferencesStore
|
||||
val customElementCallBaseUrl by appPreferencesStore
|
||||
.getCustomElementCallBaseUrlFlow()
|
||||
.collectAsState(initial = null)
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
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
|
||||
val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() && it != ElementCallConfig.DEFAULT_BASE_URL }
|
||||
preferencesStore.setCustomElementCallBaseUrl(urlToSave)
|
||||
appPreferencesStore.setCustomElementCallBaseUrl(urlToSave)
|
||||
}
|
||||
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.room.RoomNotificationMode
|
||||
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
|
||||
|
||||
open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvider<EditDefaultNotificationSettingState> {
|
||||
|
|
@ -49,14 +49,12 @@ private fun anEditDefaultNotificationSettingsState(
|
|||
)
|
||||
|
||||
private fun aRoomSummary() = RoomSummary.Filled(
|
||||
RoomSummaryDetails(
|
||||
aRoomSummaryDetails(
|
||||
roomId = RoomId("!roomId:domain"),
|
||||
name = "Room",
|
||||
avatarURLString = null,
|
||||
avatarUrl = null,
|
||||
isDirect = false,
|
||||
lastMessage = null,
|
||||
lastMessageTimestamp = null,
|
||||
unreadNotificationCount = 0,
|
||||
notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ fun EditDefaultNotificationSettingView(
|
|||
if (state.roomsWithUserDefinedMode.isNotEmpty()) {
|
||||
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_edit_custom_settings_section_title)) {
|
||||
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.MENTIONS_AND_KEYWORDS_ONLY -> {
|
||||
stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords)
|
||||
|
|
@ -95,7 +95,7 @@ fun EditDefaultNotificationSettingView(
|
|||
val avatarData = AvatarData(
|
||||
id = summary.identifier(),
|
||||
name = summary.details.name,
|
||||
url = summary.details.avatarURLString,
|
||||
url = summary.details.avatarUrl,
|
||||
size = AvatarSize.CustomRoomNotificationSetting,
|
||||
)
|
||||
ListItem(
|
||||
|
|
|
|||
|
|
@ -33,16 +33,16 @@ class DefaultVersionFormatter @Inject constructor(
|
|||
private val buildMeta: BuildMeta,
|
||||
) : VersionFormatter {
|
||||
override fun get(): String {
|
||||
return stringProvider.getString(
|
||||
val base = stringProvider.getString(
|
||||
CommonStrings.settings_version_number,
|
||||
buildMeta.versionName,
|
||||
buildMeta.versionCode.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class FakeVersionFormatter : VersionFormatter {
|
||||
override fun get(): String {
|
||||
return "A Version"
|
||||
return if (buildMeta.gitBranchName == "main") {
|
||||
base
|
||||
} else {
|
||||
// In case of a build not from main, we display the branch name and the revision
|
||||
"$base\n${buildMeta.gitBranchName}\n${buildMeta.gitRevision}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
<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_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_edit_profile_display_name">"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_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_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_edit_profile_display_name">"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_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_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_edit_profile_display_name">"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_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_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_edit_profile_display_name">"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 com.google.common.truth.Truth.assertThat
|
||||
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.awaitLastSequentialItem
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -34,8 +35,7 @@ class AdvancedSettingsPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val store = InMemoryPreferencesStore()
|
||||
val presenter = AdvancedSettingsPresenter(store)
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -43,14 +43,14 @@ class AdvancedSettingsPresenterTest {
|
|||
assertThat(initialState.isDeveloperModeEnabled).isFalse()
|
||||
assertThat(initialState.isRichTextEditorEnabled).isFalse()
|
||||
assertThat(initialState.showChangeThemeDialog).isFalse()
|
||||
assertThat(initialState.isSendPublicReadReceiptsEnabled).isTrue()
|
||||
assertThat(initialState.theme).isEqualTo(Theme.System)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - developer mode on off`() = runTest {
|
||||
val store = InMemoryPreferencesStore()
|
||||
val presenter = AdvancedSettingsPresenter(store)
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -65,8 +65,7 @@ class AdvancedSettingsPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - rich text editor on off`() = runTest {
|
||||
val store = InMemoryPreferencesStore()
|
||||
val presenter = AdvancedSettingsPresenter(store)
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.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
|
||||
fun `present - change theme`() = runTest {
|
||||
val store = InMemoryPreferencesStore()
|
||||
val presenter = AdvancedSettingsPresenter(store)
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -102,4 +115,12 @@ class AdvancedSettingsPresenterTest {
|
|||
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.featureflag.api.FeatureFlags
|
||||
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.awaitLastSequentialItem
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -114,7 +114,7 @@ class DeveloperSettingsPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - custom element call base url`() = runTest {
|
||||
val preferencesStore = InMemoryPreferencesStore()
|
||||
val preferencesStore = InMemoryAppPreferencesStore()
|
||||
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -149,14 +149,14 @@ class DeveloperSettingsPresenterTest {
|
|||
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),
|
||||
clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(),
|
||||
rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()),
|
||||
preferencesStore: InMemoryPreferencesStore = InMemoryPreferencesStore(),
|
||||
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
): DeveloperSettingsPresenter {
|
||||
return DeveloperSettingsPresenter(
|
||||
featureFlagService = featureFlagService,
|
||||
computeCacheSizeUseCase = cacheSizeUseCase,
|
||||
clearCacheUseCase = clearCacheUseCase,
|
||||
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.notificationsettings.FakeNotificationSettingsService
|
||||
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.tests.testutils.awaitLastSequentialItem
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
|
|
@ -72,11 +72,11 @@ class EditDefaultNotificationSettingsPresenterTests {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.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 ->
|
||||
state.roomsWithUserDefinedMode.any { it.details.notificationMode == RoomNotificationMode.ALL_MESSAGES }
|
||||
state.roomsWithUserDefinedMode.any { it.details.userDefinedNotificationMode == RoomNotificationMode.ALL_MESSAGES }
|
||||
}.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 {
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -73,9 +75,10 @@ private fun TakeScreenshot(
|
|||
onScreenshotTaken: (ImageResult) -> Unit
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val latestOnScreenshotTaken by rememberUpdatedState(onScreenshotTaken)
|
||||
LaunchedEffect(Unit) {
|
||||
view.screenshot {
|
||||
onScreenshotTaken(it)
|
||||
latestOnScreenshotTaken(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,4 +52,9 @@ interface BugReporter {
|
|||
* Set the current tracing filter.
|
||||
*/
|
||||
fun setCurrentTracingFilter(tracingFilter: String)
|
||||
|
||||
/**
|
||||
* Save the logcat.
|
||||
*/
|
||||
fun saveLogCat()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
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.di.AppScope
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -37,7 +38,12 @@ class BugReportNode @AssistedInject constructor(
|
|||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: BugReportPresenter,
|
||||
private val bugReporter: BugReporter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private fun onViewLogs(basePath: String) {
|
||||
plugins<BugReportEntryPoint.Callback>().forEach { it.onViewLogs(basePath) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
|
@ -50,6 +56,11 @@ class BugReportNode @AssistedInject constructor(
|
|||
activity?.toast(CommonStrings.common_report_submitted)
|
||||
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.designsystem.components.async.AsyncActionView
|
||||
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.PreferenceRow
|
||||
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.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
|
||||
|
|
@ -55,6 +57,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
@Composable
|
||||
fun BugReportView(
|
||||
state: BugReportState,
|
||||
onViewLogs: () -> Unit,
|
||||
onDone: () -> Unit,
|
||||
onBackPressed: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -97,6 +100,13 @@ fun BugReportView(
|
|||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
PreferenceDivider()
|
||||
PreferenceText(
|
||||
title = stringResource(id = R.string.screen_bug_report_view_logs),
|
||||
enabled = isFormEnabled,
|
||||
onClick = onViewLogs,
|
||||
)
|
||||
PreferenceDivider()
|
||||
PreferenceSwitch(
|
||||
isChecked = state.formState.sendLogs,
|
||||
onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) },
|
||||
|
|
@ -169,5 +179,6 @@ internal fun BugReportViewPreview(@PreviewParameter(BugReportStateProvider::clas
|
|||
state = state,
|
||||
onDone = {},
|
||||
onBackPressed = {},
|
||||
onViewLogs = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ class DefaultBugReporter @Inject constructor(
|
|||
private val logcatCommandDebug = arrayOf("logcat", "-d", "-v", "threadtime", "*:*")
|
||||
private var currentTracingFilter: String? = null
|
||||
|
||||
private val logCatErrFile = File(logDirectory().absolutePath, LOG_CAT_FILENAME)
|
||||
|
||||
override suspend fun sendBugReport(
|
||||
withDevicesLogs: Boolean,
|
||||
withCrashLogs: Boolean,
|
||||
|
|
@ -130,8 +132,8 @@ class DefaultBugReporter @Inject constructor(
|
|||
}
|
||||
|
||||
if (!isCancelled && (withCrashLogs || withDevicesLogs)) {
|
||||
val gzippedLogcat = saveLogCat()
|
||||
|
||||
saveLogCat()
|
||||
val gzippedLogcat = compressFile(logCatErrFile)
|
||||
if (null != gzippedLogcat) {
|
||||
if (gzippedFiles.size == 0) {
|
||||
gzippedFiles.add(gzippedLogcat)
|
||||
|
|
@ -199,6 +201,9 @@ class DefaultBugReporter @Inject constructor(
|
|||
|
||||
// add some github labels
|
||||
builder.addFormDataPart("label", buildMeta.versionName)
|
||||
builder.addFormDataPart("label", buildMeta.flavorDescription)
|
||||
builder.addFormDataPart("branch_name", buildMeta.gitBranchName)
|
||||
|
||||
if (crashCallStack.isNotEmpty() && withCrashLogs) {
|
||||
builder.addFormDataPart("label", "crash")
|
||||
}
|
||||
|
|
@ -320,7 +325,9 @@ class DefaultBugReporter @Inject constructor(
|
|||
}
|
||||
|
||||
override fun logDirectory(): File {
|
||||
return File(context.cacheDir, LOG_DIRECTORY_NAME)
|
||||
return File(context.cacheDir, LOG_DIRECTORY_NAME).apply {
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanLogDirectoryIfNeeded() {
|
||||
|
|
@ -380,30 +387,19 @@ class DefaultBugReporter @Inject constructor(
|
|||
*
|
||||
* @return the file if the operation succeeds
|
||||
*/
|
||||
private fun saveLogCat(): File? {
|
||||
val logCatErrFile = File(context.cacheDir.absolutePath, LOG_CAT_FILENAME)
|
||||
|
||||
override fun saveLogCat() {
|
||||
if (logCatErrFile.exists()) {
|
||||
logCatErrFile.safeDelete()
|
||||
}
|
||||
|
||||
try {
|
||||
logCatErrFile.writer().use {
|
||||
getLogCatError(it)
|
||||
}
|
||||
|
||||
return compressFile(logCatErrFile)
|
||||
} catch (error: OutOfMemoryError) {
|
||||
Timber.e(error, "## saveLogCat() : fail to write logcat OOM")
|
||||
} catch (e: Exception) {
|
||||
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_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_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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@
|
|||
<string name="screen_bug_report_include_logs">"Разрешить ведение журналов"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Отправить снимок экрана"</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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@
|
|||
<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_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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@
|
|||
<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_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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Succes
|
|||
override fun setCurrentTracingFilter(tracingFilter: String) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun saveLogCat() {
|
||||
// No op
|
||||
}
|
||||
}
|
||||
|
||||
enum class FakeBugReporterMode {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class DefaultBugReporterTest {
|
|||
val sut = createDefaultBugReporter(server)
|
||||
var onUploadCancelledCalled = false
|
||||
var onUploadFailedCalled = false
|
||||
var progressValues = mutableListOf<Int>()
|
||||
val progressValues = mutableListOf<Int>()
|
||||
var onUploadSucceedCalled = false
|
||||
sut.sendBugReport(
|
||||
withDevicesLogs = true,
|
||||
|
|
@ -80,7 +80,7 @@ class DefaultBugReporterTest {
|
|||
server.shutdown()
|
||||
assertThat(onUploadCancelledCalled).isFalse()
|
||||
assertThat(onUploadFailedCalled).isFalse()
|
||||
assertThat(progressValues.size).isEqualTo(10)
|
||||
assertThat(progressValues.size).isEqualTo(EXPECTED_NUMBER_OF_PROGRESS_VALUE)
|
||||
assertThat(onUploadSucceedCalled).isTrue()
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ class DefaultBugReporterTest {
|
|||
var onUploadCancelledCalled = false
|
||||
var onUploadFailedCalled = false
|
||||
var onUploadFailedReason: String? = null
|
||||
var progressValues = mutableListOf<Int>()
|
||||
val progressValues = mutableListOf<Int>()
|
||||
var onUploadSucceedCalled = false
|
||||
sut.sendBugReport(
|
||||
withDevicesLogs = true,
|
||||
|
|
@ -131,7 +131,7 @@ class DefaultBugReporterTest {
|
|||
assertThat(onUploadCancelledCalled).isFalse()
|
||||
assertThat(onUploadFailedCalled).isTrue()
|
||||
assertThat(onUploadFailedReason).isEqualTo("An error body")
|
||||
assertThat(progressValues.size).isEqualTo(10)
|
||||
assertThat(progressValues.size).isEqualTo(EXPECTED_NUMBER_OF_PROGRESS_VALUE)
|
||||
assertThat(onUploadSucceedCalled).isFalse()
|
||||
}
|
||||
|
||||
|
|
@ -153,4 +153,8 @@ class DefaultBugReporterTest {
|
|||
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.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
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.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
|
@ -63,6 +65,12 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
val scope = rememberCoroutineScope()
|
||||
val leaveRoomState = leaveRoomPresenter.present()
|
||||
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) {
|
||||
canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings)
|
||||
|
|
@ -70,7 +78,13 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
room.updateRoomNotificationSettings()
|
||||
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()
|
||||
|
|
@ -82,8 +96,8 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
|
||||
val roomType by getRoomType(dmMember)
|
||||
|
||||
val topicState = remember(canEditTopic, room.topic, roomType) {
|
||||
val topic = room.topic
|
||||
val topicState = remember(canEditTopic, roomTopic, roomType) {
|
||||
val topic = roomTopic
|
||||
|
||||
when {
|
||||
!topic.isNullOrBlank() -> RoomTopicState.ExistingTopic(topic)
|
||||
|
|
@ -115,9 +129,9 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
|
||||
return RoomDetailsState(
|
||||
roomId = room.roomId.value,
|
||||
roomName = room.displayName,
|
||||
roomName = roomName,
|
||||
roomAlias = room.alias,
|
||||
roomAvatarUrl = room.avatarUrl,
|
||||
roomAvatarUrl = roomAvatar,
|
||||
roomTopic = topicState,
|
||||
memberCount = room.joinedMemberCount,
|
||||
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