Merge branch 'develop' into feature/fga/pinned_messages_list
This commit is contained in:
commit
12e7e05551
289 changed files with 3558 additions and 1883 deletions
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
|
|
@ -79,7 +79,7 @@ jobs:
|
|||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: elementx-apk-maestro
|
||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.8.1
|
||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.9.1
|
||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||
with:
|
||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
concurrency:
|
||||
group: ${{ format('build-release-main-gplay-{0}', github.sha) }}
|
||||
group: ${{ format('build-release-main-enterprise-{0}', github.sha) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
appId: ${MAESTRO_APP_ID}
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible: "Confirm that it's you"
|
||||
visible: "Confirm your identity"
|
||||
timeout: 20000
|
||||
|
|
|
|||
89
CHANGES.md
89
CHANGES.md
|
|
@ -1,3 +1,92 @@
|
|||
Changes in Element X v0.5.1 (2024-08-28)
|
||||
=========================================
|
||||
|
||||
### ✨ Features
|
||||
* Add simplified sliding sync toggle to developer options by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3222
|
||||
* Feature: identity reset by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3298
|
||||
* Timeline UI | MessageShield Support by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/3240
|
||||
* Suggestion for room alias (disabled for now) by @bmarty in https://github.com/element-hq/element-x-android/pull/3322
|
||||
* Allow `PictureInPicture` mode for Element Call. by @bmarty in https://github.com/element-hq/element-x-android/pull/3345
|
||||
|
||||
### 🙌 Improvements
|
||||
* Join Room : allow to join by alias (and getPreview) by @ganfra in https://github.com/element-hq/element-x-android/pull/3241
|
||||
* [Feature] Pinned message : render m.room.pinned events in timeline by @ganfra in https://github.com/element-hq/element-x-android/pull/3276
|
||||
* Enable sync on push feature flag to partially sync when notifications arrive by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3290
|
||||
* Improve the text for mentions and replies in notifications by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3328
|
||||
* Use new functions exposed by Element Call about PiP by @bmarty in https://github.com/element-hq/element-x-android/pull/3334
|
||||
|
||||
### 🐛 Bugfixes
|
||||
* Ensure sessionPath is not reused for different homeserver. Fixes not loading media issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/3299
|
||||
* Fix reset identity with password stuck in loading state. by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3317
|
||||
|
||||
### 🗣 Translations
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3252
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3267
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3297
|
||||
* Sync Strings - New language: Dutch. by @ElementBot in https://github.com/element-hq/element-x-android/pull/3308
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3339
|
||||
|
||||
### 🧱 Build
|
||||
* Update sonarcloud project key by @guillaumevillemont in https://github.com/element-hq/element-x-android/pull/3264
|
||||
* Fix `build_rust_sdk.sh` script to work on linux by @erikjohnston in https://github.com/element-hq/element-x-android/pull/3291
|
||||
* Fix proguard config for nightly and release builds by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3294
|
||||
* Gradle update action: Use JDK 17 and skip early in forks. by @bmarty in https://github.com/element-hq/element-x-android/pull/3311
|
||||
* Gradle update action: add label and use other token. by @bmarty in https://github.com/element-hq/element-x-android/pull/3313
|
||||
* Update Gradle Wrapper from 8.9 to 8.10 by @ElementBot in https://github.com/element-hq/element-x-android/pull/3314
|
||||
|
||||
### 🚧 In development 🚧
|
||||
* WIP Pinned events : add feature flag and pin/unpin actions by @ganfra in https://github.com/element-hq/element-x-android/pull/3255
|
||||
* WIP Pinned events : start creating the banner ui, no logic. by @ganfra in https://github.com/element-hq/element-x-android/pull/3259
|
||||
* WIP Pinned events : banner logic by @ganfra in https://github.com/element-hq/element-x-android/pull/3275
|
||||
|
||||
### Dependency upgrades
|
||||
* Update dependency org.maplibre.gl:android-sdk to v11.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3244
|
||||
* Update activity to v1.9.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3242
|
||||
* Update media3 to v1.4.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3247
|
||||
* Update dependency androidx.annotation:annotation-jvm to v1.8.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3243
|
||||
* Update dependencyAnalysis to v1.33.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3250
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.35 by @renovate in https://github.com/element-hq/element-x-android/pull/3249
|
||||
* Update dependency io.sentry:sentry-android to v7.12.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3246
|
||||
* Update dependency io.nlopez.compose.rules:detekt to v0.4.8 by @renovate in https://github.com/element-hq/element-x-android/pull/3254
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.36 by @renovate in https://github.com/element-hq/element-x-android/pull/3269
|
||||
* Update wysiwyg to v2.37.8 by @renovate in https://github.com/element-hq/element-x-android/pull/3263
|
||||
* Update dependency io.sentry:sentry-android to v7.13.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3258
|
||||
* Update dependency io.nlopez.compose.rules:detekt to v0.4.9 by @renovate in https://github.com/element-hq/element-x-android/pull/3277
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.38 by @renovate in https://github.com/element-hq/element-x-android/pull/3280
|
||||
* Update dependency androidx.annotation:annotation-jvm to v1.8.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3282
|
||||
* Update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/2990
|
||||
* Update dependency io.nlopez.compose.rules:detekt to v0.4.10 by @renovate in https://github.com/element-hq/element-x-android/pull/3281
|
||||
* Update dependency com.posthog:posthog-android to v3.5.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3287
|
||||
* Update wysiwyg to v2.37.8 by @renovate in https://github.com/element-hq/element-x-android/pull/3284
|
||||
* Update the SDK bindings to `v0.2.39` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3288
|
||||
* Update gradle/actions action to v4 by @renovate in https://github.com/element-hq/element-x-android/pull/3265
|
||||
* Update android.gradle.plugin to v8.5.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3004
|
||||
* Update dependency io.sentry:sentry-android to v7.13.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3285
|
||||
* Update dependency io.sentry:sentry-android to v7.14.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3302
|
||||
* Update dependency androidx.test:runner to v1.6.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3304
|
||||
* Update dependency com.otaliastudios:transcoder to v0.11.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3306
|
||||
* Update lifecycle to v2.8.0 by @renovate in https://github.com/element-hq/element-x-android/pull/2848
|
||||
* Update lifecycle to v2.8.4 by @renovate in https://github.com/element-hq/element-x-android/pull/3315
|
||||
* Update dagger to v2.52 by @renovate in https://github.com/element-hq/element-x-android/pull/3270
|
||||
* Update telephoto to v0.13.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3325
|
||||
* Update dependency androidx.compose:compose-bom to v2024.08.00 by @renovate in https://github.com/element-hq/element-x-android/pull/3323
|
||||
* Update dependency com.google.firebase:firebase-bom to v33.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3331
|
||||
* Update dependency com.posthog:posthog-android to v3.5.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3340
|
||||
* Update dependency com.android.tools:desugar_jdk_libs to v2.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3341
|
||||
* Update dependencyAnalysis to v2 (major) by @renovate in https://github.com/element-hq/element-x-android/pull/3346
|
||||
* Update dependency org.maplibre.gl:android-sdk to v11.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3347
|
||||
* Update media3 to v1.4.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3344
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.40 by @renovate in https://github.com/element-hq/element-x-android/pull/3343
|
||||
|
||||
### Others
|
||||
* Feature/fga/push subscribe to room by @ganfra in https://github.com/element-hq/element-x-android/pull/3257
|
||||
* Feature/fga/start sync on push by @ganfra in https://github.com/element-hq/element-x-android/pull/3260
|
||||
* Cleanup and add unit test for DefaultPinnedMessagesBannerFormatter by @bmarty in https://github.com/element-hq/element-x-android/pull/3307
|
||||
* Add test on function name which may start or end with spaces by @bmarty in https://github.com/element-hq/element-x-android/pull/3318
|
||||
* Fix broken direct room member for rooms with old users that left by @networkException in https://github.com/element-hq/element-x-android/pull/3324
|
||||
* Add unit test on MatrixRoom extension by @bmarty in https://github.com/element-hq/element-x-android/pull/3327
|
||||
* Fix login navigation getting stuck when the app was compiled with no-op analytics provider by @SpiritCroc in https://github.com/element-hq/element-x-android/pull/3337
|
||||
|
||||
Changes in Element X v0.5.0 (2024-07-24)
|
||||
=========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -17,15 +17,19 @@
|
|||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
|
||||
import com.android.build.gradle.internal.tasks.factory.dependsOn
|
||||
import com.android.build.gradle.tasks.GenerateBuildConfig
|
||||
import extension.AssetCopyTask
|
||||
import extension.GitBranchNameValueSource
|
||||
import extension.GitRevisionValueSource
|
||||
import extension.allEnterpriseImpl
|
||||
import extension.allFeaturesImpl
|
||||
import extension.allLibrariesImpl
|
||||
import extension.allServicesImpl
|
||||
import extension.gitBranchName
|
||||
import extension.gitRevision
|
||||
import extension.koverDependencies
|
||||
import extension.locales
|
||||
import extension.setupKover
|
||||
import java.util.Locale
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-application")
|
||||
|
|
@ -36,7 +40,8 @@ plugins {
|
|||
id(libs.plugins.firebaseAppDistribution.get().pluginId)
|
||||
alias(libs.plugins.knit)
|
||||
id("kotlin-parcelize")
|
||||
id("com.google.android.gms.oss-licenses-plugin")
|
||||
alias(libs.plugins.licensee)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
// To be able to update the firebase.xml files, uncomment and build the project
|
||||
// id("com.google.gms.google-services")
|
||||
}
|
||||
|
|
@ -61,9 +66,6 @@ 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.
|
||||
|
|
@ -215,6 +217,9 @@ androidComponents {
|
|||
output.versionCode.set((output.versionCode.orNull ?: 0) * 10 + abiCode)
|
||||
}
|
||||
}
|
||||
|
||||
val reportingExtension: ReportingExtension = project.extensions.getByType(ReportingExtension::class.java)
|
||||
configureLicensesTasks(reportingExtension)
|
||||
}
|
||||
|
||||
// Knit
|
||||
|
|
@ -259,8 +264,6 @@ dependencies {
|
|||
// Comment to not include unified push in the project
|
||||
implementation(projects.libraries.pushproviders.unifiedpush)
|
||||
|
||||
"gplayImplementation"(libs.play.services.oss.licenses)
|
||||
|
||||
implementation(libs.appyx.core)
|
||||
implementation(libs.androidx.splash)
|
||||
implementation(libs.androidx.core)
|
||||
|
|
@ -291,3 +294,51 @@ dependencies {
|
|||
|
||||
koverDependencies()
|
||||
}
|
||||
|
||||
tasks.withType<GenerateBuildConfig>().configureEach {
|
||||
outputs.upToDateWhen { false }
|
||||
val gitRevision = providers.of(GitRevisionValueSource::class.java) {}.get()
|
||||
val gitBranchName = providers.of(GitBranchNameValueSource::class.java) {}.get()
|
||||
android.defaultConfig.buildConfigField("String", "GIT_REVISION", "\"$gitRevision\"")
|
||||
android.defaultConfig.buildConfigField("String", "GIT_BRANCH_NAME", "\"$gitBranchName\"")
|
||||
}
|
||||
|
||||
licensee {
|
||||
allow("Apache-2.0")
|
||||
allow("MIT")
|
||||
allow("GPL-2.0-with-classpath-exception")
|
||||
allow("BSD-2-Clause")
|
||||
allowUrl("https://opensource.org/licenses/MIT")
|
||||
allowUrl("https://developer.android.com/studio/terms.html")
|
||||
allowUrl("http://openjdk.java.net/legal/gplv2+ce.html")
|
||||
allowUrl("https://www.zetetic.net/sqlcipher/license/")
|
||||
allowUrl("https://jsoup.org/license")
|
||||
allowUrl("https://asm.ow2.io/license.html")
|
||||
ignoreDependencies("com.github.matrix-org", "matrix-analytics-events")
|
||||
}
|
||||
|
||||
fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) {
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
val capitalizedVariantName = variant.name.replaceFirstChar {
|
||||
if (it.isLowerCase()) {
|
||||
it.titlecase(Locale.getDefault())
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
val artifactsFile = reportingExtension.file("licensee/android$capitalizedVariantName/artifacts.json")
|
||||
|
||||
val copyArtifactsTask =
|
||||
project.tasks.register<AssetCopyTask>("copy${capitalizedVariantName}LicenseeReportToAssets") {
|
||||
inputFile.set(artifactsFile)
|
||||
targetFileName.set("licensee-artifacts.json")
|
||||
}
|
||||
variant.sources.assets?.addGeneratedSourceDirectory(
|
||||
copyArtifactsTask,
|
||||
AssetCopyTask::outputDirectory,
|
||||
)
|
||||
copyArtifactsTask.dependsOn("licenseeAndroid$capitalizedVariantName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
||||
android:theme="@style/Theme.OssLicenses" />
|
||||
<activity
|
||||
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
|
||||
android:theme="@style/Theme.OssLicenses" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.licenses
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class OssOpenSourcesLicensesProvider @Inject constructor() : OpenSourceLicensesProvider {
|
||||
override val hasOpenSourceLicenses: Boolean = true
|
||||
|
||||
override fun navigateToOpenSourceLicenses(activity: Activity) {
|
||||
val title = activity.getString(CommonStrings.common_open_source_licenses)
|
||||
OssLicensesMenuActivity.setActivityTitle(title)
|
||||
activity.startActivity(Intent(activity, OssLicensesMenuActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<!-- Use a few colors from compoundColorsDark -->
|
||||
<!-- DarkColorTokens.colorThemeBg -->
|
||||
<color name="colorThemeBg">#FF101317</color>
|
||||
<!-- DarkColorTokens.colorGray1400 -->
|
||||
<color name="textPrimary">#FFEBEEF2</color>
|
||||
<!-- DarkColorTokens.colorGray900 -->
|
||||
<color name="textSecondary">#ff808994</color>
|
||||
<!-- DarkColorTokens.colorBlue900 -->
|
||||
<color name="textLinkExternal">#FF4187EB</color>
|
||||
|
||||
<bool name="windowLightStatusBar">false</bool>
|
||||
<bool name="windowLightNavigationBar">false</bool>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="Theme.OssLicenses.Light.v27" parent="Base.Theme.OssLicenses">
|
||||
<item name="android:windowLightNavigationBar">@bool/windowLightNavigationBar</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.OssLicenses" parent="Theme.OssLicenses.Light.v27"/>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<!-- Use a few colors from compoundColorsLight -->
|
||||
<!-- LightColorTokens.colorThemeBg -->
|
||||
<color name="colorThemeBg">#FFFFFFFF</color>
|
||||
<!-- LightColorTokens.colorGray1400 -->
|
||||
<color name="textPrimary">#FF1B1D22</color>
|
||||
<!-- LightColorTokens.colorGray900 -->
|
||||
<color name="textSecondary">#FF656D77</color>
|
||||
<!-- LightColorTokens.colorBlue900 -->
|
||||
<color name="textLinkExternal">#FF0467DD</color>
|
||||
|
||||
<bool name="windowLightStatusBar">true</bool>
|
||||
<bool name="windowLightNavigationBar">true</bool>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="NoElevationToolbar" parent="Widget.MaterialComponents.Toolbar">
|
||||
<item name="android:elevation">0dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="Base.Theme.OssLicenses" parent="Theme.MaterialComponents.DayNight">
|
||||
<!-- Background of title bar -->
|
||||
<item name="colorPrimary">@color/colorThemeBg</item>
|
||||
<!-- Background of the screen -->
|
||||
<item name="android:colorBackground">@color/colorThemeBg</item>
|
||||
<!-- Text of the licenses -->
|
||||
<item name="android:textColor">@color/textSecondary</item>
|
||||
<!-- Link text color -->
|
||||
<item name="android:textColorLink">@color/textLinkExternal</item>
|
||||
<!-- Title, back button and license item text color -->
|
||||
<item name="android:textColorPrimary">@color/textPrimary</item>
|
||||
<!-- Background of status bar -->
|
||||
<item name="android:statusBarColor">@color/colorThemeBg</item>
|
||||
<item name="android:windowLightStatusBar">@bool/windowLightStatusBar</item>
|
||||
<!-- Background of navigation bar -->
|
||||
<item name="android:navigationBarColor">@color/colorThemeBg</item>
|
||||
<!-- Try to remove Toolbar elevation, but it does not work :/ -->
|
||||
<item name="toolbarStyle">@style/NoElevationToolbar</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.OssLicenses" parent="Base.Theme.OssLicenses" />
|
||||
|
||||
</resources>
|
||||
|
|
@ -261,6 +261,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
backstack.push(NavTarget.CreateRoom)
|
||||
}
|
||||
|
||||
override fun onSetUpRecoveryClick() {
|
||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.SetUpRecovery))
|
||||
}
|
||||
|
||||
override fun onSessionConfirmRecoveryKeyClick() {
|
||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import com.google.devtools.ksp.gradle.KspTask
|
||||
import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath(libs.kotlin.gradle.plugin)
|
||||
classpath(libs.gms.google.services)
|
||||
classpath(libs.oss.licenses.plugin)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +57,7 @@ allprojects {
|
|||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.10")
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.11")
|
||||
}
|
||||
|
||||
// KtLint
|
||||
|
|
@ -202,24 +198,6 @@ subprojects {
|
|||
tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask)
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/airbnb/Showkase/issues/335
|
||||
subprojects {
|
||||
tasks.withType<KspTask> {
|
||||
doLast {
|
||||
fileTree(layout.buildDirectory).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file ->
|
||||
ReplaceRegExp().apply {
|
||||
setMatch("^public fun Showkase.getMetadata")
|
||||
setReplace("@Suppress(\"DEPRECATION\") public fun Showkase.getMetadata")
|
||||
setFlags("g")
|
||||
setByLine(true)
|
||||
setFile(file)
|
||||
execute()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
<string name="call_foreground_service_channel_title_android">"Chamada em curso"</string>
|
||||
<string name="call_foreground_service_message_android">"Toca para voltar à chamada"</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ Chamada em curso"</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"A receber chamada da Element "</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"A receber chamada da Element"</string>
|
||||
</resources>
|
||||
|
|
|
|||
28
features/licenses/api/build.gradle.kts
Normal file
28
features/licenses/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.licenses.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
}
|
||||
|
|
@ -14,11 +14,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.api
|
||||
package io.element.android.features.licenses.api
|
||||
|
||||
import android.app.Activity
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
|
||||
interface OpenSourceLicensesProvider {
|
||||
val hasOpenSourceLicenses: Boolean
|
||||
fun navigateToOpenSourceLicenses(activity: Activity)
|
||||
interface OpenSourceLicensesEntryPoint {
|
||||
fun getNode(node: Node, buildContext: BuildContext): Node
|
||||
}
|
||||
49
features/licenses/impl/build.gradle.kts
Normal file
49
features/licenses/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("kotlin-parcelize")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.licenses.impl"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(libs.serialization.json)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.features.licenses.api)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.coroutines.core)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
@ -14,19 +14,19 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.x.licenses
|
||||
package io.element.android.features.licenses.impl
|
||||
|
||||
import android.app.Activity
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||
import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class FdroidOpenSourceLicensesProvider @Inject constructor() : OpenSourceLicensesProvider {
|
||||
override val hasOpenSourceLicenses: Boolean = false
|
||||
|
||||
override fun navigateToOpenSourceLicenses(activity: Activity) {
|
||||
error("Not supported, please ensure that hasOpenSourcesLicenses is true before calling this method")
|
||||
class DefaultOpenSourcesLicensesEntryPoint @Inject constructor() : OpenSourceLicensesEntryPoint {
|
||||
override fun getNode(node: Node, buildContext: BuildContext): Node {
|
||||
return node.createNode<DependenciesFlowNode>(buildContext)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.licenses.impl
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.licenses.impl.details.DependenciesDetailsNode
|
||||
import io.element.android.features.licenses.impl.list.DependencyLicensesListNode
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class DependenciesFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : BaseFlowNode<DependenciesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.LicensesList,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object LicensesList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class LicenseDetails(val license: DependencyLicenseItem) : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
is NavTarget.LicensesList -> {
|
||||
val callback = object : DependencyLicensesListNode.Callback {
|
||||
override fun onOpenLicense(license: DependencyLicenseItem) {
|
||||
backstack.push(NavTarget.LicenseDetails(license))
|
||||
}
|
||||
}
|
||||
createNode<DependencyLicensesListNode>(buildContext, listOf(callback))
|
||||
}
|
||||
is NavTarget.LicenseDetails -> {
|
||||
createNode<DependenciesDetailsNode>(buildContext, listOf(DependenciesDetailsNode.Inputs(navTarget.license)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView(modifier)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.licenses.impl
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import javax.inject.Inject
|
||||
|
||||
interface LicensesProvider {
|
||||
suspend fun provides(): List<DependencyLicenseItem>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AssetLicensesProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : LicensesProvider {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
override suspend fun provides(): List<DependencyLicenseItem> {
|
||||
return withContext(dispatchers.io) {
|
||||
context.assets.open("licensee-artifacts.json").use { inputStream ->
|
||||
val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
}
|
||||
json.decodeFromStream<List<DependencyLicenseItem>>(inputStream)
|
||||
.sortedBy { it.safeName.lowercase() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* 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
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.securebackup.impl.createkey
|
||||
package io.element.android.features.licenses.impl.details
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -24,21 +24,31 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class CreateNewRecoveryKeyNode @AssistedInject constructor(
|
||||
@ContributesNode(AppScope::class)
|
||||
class DependenciesDetailsNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
) : Node(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
data class Inputs(
|
||||
val licenseItem: DependencyLicenseItem,
|
||||
) : NodeInputs
|
||||
|
||||
private val licenseItem = inputs<Inputs>().licenseItem
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
CreateNewRecoveryKeyView(
|
||||
desktopApplicationName = buildMeta.desktopApplicationName,
|
||||
DependenciesDetailsView(
|
||||
modifier = modifier,
|
||||
onBackClick = ::navigateUp,
|
||||
licenseItem = licenseItem,
|
||||
onBack = ::navigateUp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.licenses.impl.details
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.features.licenses.impl.list.aDependencyLicenseItem
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DependenciesDetailsView(
|
||||
licenseItem: DependencyLicenseItem,
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = licenseItem.safeName) },
|
||||
navigationIcon = { BackButton(onClick = onBack) },
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
) {
|
||||
val licenses = licenseItem.licenses.orEmpty() +
|
||||
licenseItem.unknownLicenses.orEmpty()
|
||||
items(licenses) { license ->
|
||||
val text = buildString {
|
||||
if (license.name != null) {
|
||||
append(license.name)
|
||||
append("\n")
|
||||
append("\n")
|
||||
}
|
||||
if (license.url != null) {
|
||||
append(license.url)
|
||||
}
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
ClickableLinkText(
|
||||
text = text,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun DependenciesDetailsViewPreview() = ElementPreview {
|
||||
DependenciesDetailsView(
|
||||
licenseItem = aDependencyLicenseItem(),
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.licenses.impl.list
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class DependencyLicensesListNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: DependencyLicensesListPresenter,
|
||||
) : Node(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
interface Callback : Plugin {
|
||||
fun onOpenLicense(license: DependencyLicenseItem)
|
||||
}
|
||||
|
||||
private fun onOpenLicense(license: DependencyLicenseItem) {
|
||||
plugins<Callback>()
|
||||
.forEach { it.onOpenLicense(license) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
DependencyLicensesListView(
|
||||
state = state,
|
||||
onBackClick = ::navigateUp,
|
||||
onOpenLicense = ::onOpenLicense,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.licenses.impl.list
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.licenses.impl.LicensesProvider
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import javax.inject.Inject
|
||||
|
||||
class DependencyLicensesListPresenter @Inject constructor(
|
||||
private val licensesProvider: LicensesProvider,
|
||||
) : Presenter<DependencyLicensesListState> {
|
||||
@Composable
|
||||
override fun present(): DependencyLicensesListState {
|
||||
var licenses by remember {
|
||||
mutableStateOf<AsyncData<ImmutableList<DependencyLicenseItem>>>(AsyncData.Loading())
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
runCatching {
|
||||
licenses = AsyncData.Success(licensesProvider.provides().toPersistentList())
|
||||
}.onFailure {
|
||||
licenses = AsyncData.Failure(it)
|
||||
}
|
||||
}
|
||||
return DependencyLicensesListState(licenses = licenses)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,13 +14,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.about
|
||||
package io.element.android.features.licenses.impl.list
|
||||
|
||||
import android.app.Activity
|
||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
class FakeOpenSourceLicensesProvider(
|
||||
override val hasOpenSourceLicenses: Boolean,
|
||||
) : OpenSourceLicensesProvider {
|
||||
override fun navigateToOpenSourceLicenses(activity: Activity) = Unit
|
||||
}
|
||||
data class DependencyLicensesListState(
|
||||
val licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
|
||||
)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.licenses.impl.list
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.features.licenses.impl.model.License
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class DependencyLicensesListStateProvider : PreviewParameterProvider<DependencyLicensesListState> {
|
||||
override val values: Sequence<DependencyLicensesListState>
|
||||
get() = sequenceOf(
|
||||
DependencyLicensesListState(
|
||||
licenses = AsyncData.Loading()
|
||||
),
|
||||
DependencyLicensesListState(
|
||||
licenses = AsyncData.Failure(Exception("Failed to load licenses"))
|
||||
),
|
||||
DependencyLicensesListState(
|
||||
licenses = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aDependencyLicenseItem(),
|
||||
aDependencyLicenseItem(name = null),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aDependencyLicenseItem(
|
||||
name: String? = "A dependency",
|
||||
) = DependencyLicenseItem(
|
||||
groupId = "org.some.group",
|
||||
artifactId = "a-dependency",
|
||||
version = "1.0.0",
|
||||
name = name,
|
||||
licenses = listOf(
|
||||
License(
|
||||
identifier = "Apache 2.0",
|
||||
name = "Apache 2.0",
|
||||
url = "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
)
|
||||
),
|
||||
unknownLicenses = listOf(),
|
||||
scm = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.licenses.impl.list
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DependencyLicensesListView(
|
||||
state: DependencyLicensesListState,
|
||||
onBackClick: () -> Unit,
|
||||
onOpenLicense: (DependencyLicenseItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(CommonStrings.common_open_source_licenses)) },
|
||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
when (state.licenses) {
|
||||
is AsyncData.Failure -> item {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.common_error),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
AsyncData.Uninitialized,
|
||||
is AsyncData.Loading -> item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 64.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
is AsyncData.Success -> items(state.licenses.data) { license ->
|
||||
ListItem(
|
||||
headlineContent = { Text(license.safeName) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
buildString {
|
||||
append(license.groupId)
|
||||
append(":")
|
||||
append(license.artifactId)
|
||||
append(":")
|
||||
append(license.version)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onOpenLicense(license)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun DependencyLicensesListViewPreview(
|
||||
@PreviewParameter(DependencyLicensesListStateProvider::class) state: DependencyLicensesListState
|
||||
) = ElementPreview {
|
||||
DependencyLicensesListView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onOpenLicense = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.licenses.impl.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class DependencyLicenseItem(
|
||||
val groupId: String,
|
||||
val artifactId: String,
|
||||
val version: String,
|
||||
@SerialName("spdxLicenses")
|
||||
val licenses: List<License>?,
|
||||
val unknownLicenses: List<License>?,
|
||||
val name: String?,
|
||||
val scm: Scm?,
|
||||
) : Parcelable {
|
||||
@IgnoredOnParcel
|
||||
val safeName = name?.takeIf { name -> name != "null" } ?: "$groupId:$artifactId"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class License(
|
||||
val identifier: String?,
|
||||
val name: String?,
|
||||
val url: String?,
|
||||
) : Parcelable
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class Scm(
|
||||
val url: String,
|
||||
) : Parcelable
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.licenses.impl.list
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DependencyLicensesListPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state, no licenses`() = runTest {
|
||||
val presenter = createPresenter { emptyList() }
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.licenses).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.licenses.isSuccess()).isTrue()
|
||||
assertThat(finalState.licenses.dataOrNull()).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state, one license`() = runTest {
|
||||
val anItem = aDependencyLicenseItem()
|
||||
val presenter = createPresenter {
|
||||
listOf(anItem)
|
||||
}
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.licenses).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.licenses.isSuccess()).isTrue()
|
||||
assertThat(finalState.licenses.dataOrNull()!!.size).isEqualTo(1)
|
||||
assertThat(finalState.licenses.dataOrNull()!!.get(0)).isEqualTo(anItem)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
provideResult: () -> List<DependencyLicenseItem>
|
||||
) = DependencyLicensesListPresenter(
|
||||
licensesProvider = FakeLicensesProvider(provideResult),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.licenses.impl.list
|
||||
|
||||
import io.element.android.features.licenses.impl.LicensesProvider
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeLicensesProvider(
|
||||
private val provideResult: () -> List<DependencyLicenseItem> = { lambdaError() }
|
||||
) : LicensesProvider {
|
||||
override suspend fun provides(): List<DependencyLicenseItem> {
|
||||
return provideResult()
|
||||
}
|
||||
}
|
||||
|
|
@ -16,9 +16,11 @@
|
|||
|
||||
package io.element.android.features.lockscreen.impl.unlock
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
|
|
@ -26,6 +28,8 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.logout.api.util.onSuccessLogout
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
|
|
@ -47,6 +51,8 @@ class PinUnlockNode @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val activity = LocalContext.current as Activity
|
||||
val isDark = ElementTheme.isLightTheme.not()
|
||||
LaunchedEffect(state.isUnlocked) {
|
||||
if (state.isUnlocked) {
|
||||
onUnlock()
|
||||
|
|
@ -57,6 +63,7 @@ class PinUnlockNode @AssistedInject constructor(
|
|||
// UnlockNode is only used for in-app unlock, so we can safely set isInAppUnlock to true.
|
||||
// It's set to false in PinUnlockActivity.
|
||||
isInAppUnlock = true,
|
||||
onSuccessLogout = { onSuccessLogout(activity, isDark, it) },
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
|||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||
import io.element.android.features.logout.api.LogoutUseCase
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
|
|
@ -61,7 +62,7 @@ class PinUnlockPresenter @Inject constructor(
|
|||
mutableStateOf(false)
|
||||
}
|
||||
val signOutAction = remember {
|
||||
mutableStateOf<AsyncData<String?>>(AsyncData.Uninitialized)
|
||||
mutableStateOf<AsyncAction<String?>>(AsyncAction.Uninitialized)
|
||||
}
|
||||
var biometricUnlockResult by remember {
|
||||
mutableStateOf<BiometricUnlock.AuthenticationResult?>(null)
|
||||
|
|
@ -177,7 +178,7 @@ class PinUnlockPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
|
||||
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<String?>>) = launch {
|
||||
suspend {
|
||||
logoutUseCase.logout(ignoreSdkError = true)
|
||||
}.runCatchingUpdatingState(signOutAction)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.lockscreen.impl.unlock
|
|||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError
|
||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class PinUnlockState(
|
||||
|
|
@ -26,7 +27,7 @@ data class PinUnlockState(
|
|||
val showWrongPinTitle: Boolean,
|
||||
val remainingAttempts: AsyncData<Int>,
|
||||
val showSignOutPrompt: Boolean,
|
||||
val signOutAction: AsyncData<String?>,
|
||||
val signOutAction: AsyncAction<String?>,
|
||||
val showBiometricUnlock: Boolean,
|
||||
val isUnlocked: Boolean,
|
||||
val biometricUnlockResult: BiometricUnlock.AuthenticationResult?,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.lockscreen.impl.unlock
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
|
||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
|
||||
|
|
@ -30,7 +31,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
|
|||
aPinUnlockState(showSignOutPrompt = true),
|
||||
aPinUnlockState(showBiometricUnlock = false),
|
||||
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
|
||||
aPinUnlockState(signOutAction = AsyncData.Loading()),
|
||||
aPinUnlockState(signOutAction = AsyncAction.Loading),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ fun aPinUnlockState(
|
|||
showBiometricUnlock: Boolean = true,
|
||||
biometricUnlockResult: BiometricUnlock.AuthenticationResult? = null,
|
||||
isUnlocked: Boolean = false,
|
||||
signOutAction: AsyncData<String?> = AsyncData.Uninitialized,
|
||||
signOutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
|
||||
) = PinUnlockState(
|
||||
pinEntry = AsyncData.Success(pinEntry),
|
||||
showWrongPinTitle = showWrongPinTitle,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@ import androidx.compose.material.icons.filled.Lock
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
|
|
@ -56,6 +58,7 @@ import io.element.android.features.lockscreen.impl.components.PinEntryTextField
|
|||
import io.element.android.features.lockscreen.impl.pin.model.PinDigit
|
||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypad
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
|
|
@ -74,6 +77,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
fun PinUnlockView(
|
||||
state: PinUnlockState,
|
||||
isInAppUnlock: Boolean,
|
||||
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
OnLifecycleEvent { _, event ->
|
||||
|
|
@ -91,9 +95,21 @@ fun PinUnlockView(
|
|||
onDismiss = { state.eventSink(PinUnlockEvents.ClearSignOutPrompt) },
|
||||
)
|
||||
}
|
||||
if (state.signOutAction is AsyncData.Loading) {
|
||||
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
|
||||
when (state.signOutAction) {
|
||||
AsyncAction.Loading -> {
|
||||
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout)
|
||||
LaunchedEffect(state) {
|
||||
latestOnSuccessLogout(state.signOutAction.data)
|
||||
}
|
||||
}
|
||||
AsyncAction.Confirming,
|
||||
is AsyncAction.Failure,
|
||||
AsyncAction.Uninitialized -> Unit
|
||||
}
|
||||
|
||||
if (state.showBiometricUnlockError) {
|
||||
ErrorDialog(
|
||||
content = state.biometricUnlockErrorMessage ?: "",
|
||||
|
|
@ -363,6 +379,7 @@ internal fun PinUnlockViewInAppPreview(@PreviewParameter(PinUnlockStateProvider:
|
|||
PinUnlockView(
|
||||
state = state,
|
||||
isInAppUnlock = true,
|
||||
onSuccessLogout = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -374,6 +391,7 @@ internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::clas
|
|||
PinUnlockView(
|
||||
state = state,
|
||||
isInAppUnlock = false,
|
||||
onSuccessLogout = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,13 @@ import androidx.activity.compose.setContent
|
|||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter
|
||||
import io.element.android.features.lockscreen.impl.unlock.PinUnlockView
|
||||
import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings
|
||||
import io.element.android.features.logout.api.util.onSuccessLogout
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
|
|
@ -53,7 +55,12 @@ class PinUnlockActivity : AppCompatActivity() {
|
|||
setContent {
|
||||
ElementThemeApp(appPreferencesStore) {
|
||||
val state = presenter.present()
|
||||
PinUnlockView(state = state, isInAppUnlock = false)
|
||||
val isDark = ElementTheme.isLightTheme.not()
|
||||
PinUnlockView(
|
||||
state = state,
|
||||
isInAppUnlock = false,
|
||||
onSuccessLogout = { onSuccessLogout(this, isDark, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ package io.element.android.features.lockscreen.impl.unlock.di
|
|||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesTo(SessionScope::class)
|
||||
@ContributesTo(AppScope::class)
|
||||
interface PinUnlockBindings {
|
||||
fun inject(activity: PinUnlockActivity)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
|||
import io.element.android.features.lockscreen.impl.pin.model.assertText
|
||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||
import io.element.android.features.logout.test.FakeLogoutUseCase
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
|
|
@ -51,7 +52,7 @@ class PinUnlockPresenterTest {
|
|||
assertThat(state.showWrongPinTitle).isFalse()
|
||||
assertThat(state.showSignOutPrompt).isFalse()
|
||||
assertThat(state.isUnlocked).isFalse()
|
||||
assertThat(state.signOutAction).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||
assertThat(state.signOutAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
|
|
@ -106,7 +107,7 @@ class PinUnlockPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - forgot pin flow`() = runTest {
|
||||
val signOutLambda = lambdaRecorder<Boolean, String> { "" }
|
||||
val signOutLambda = lambdaRecorder<Boolean, String?> { "" }
|
||||
val signOut = FakeLogoutUseCase(signOutLambda)
|
||||
val presenter = createPinUnlockPresenter(this, logoutUseCase = signOut)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
|
|
@ -133,7 +134,7 @@ class PinUnlockPresenterTest {
|
|||
}
|
||||
skipItems(2)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(state.signOutAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
}
|
||||
assert(signOutLambda).isCalledOnce()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ sealed class ChangeServerError : Throwable() {
|
|||
|
||||
companion object {
|
||||
fun from(error: Throwable): ChangeServerError = when (error) {
|
||||
is AuthenticationException.SlidingSyncNotAvailable -> SlidingSyncAlert
|
||||
is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert
|
||||
else -> Error(R.string.screen_change_server_error_invalid_homeserver)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class ErrorFormatterTest {
|
|||
|
||||
@Test
|
||||
fun `loginError - invalid auth error returns unknown error message`() {
|
||||
val error = AuthenticationException.SlidingSyncNotAvailable("Some message. Also contains M_FORBIDDEN, but won't be parsed")
|
||||
val error = AuthenticationException.SlidingSyncVersion("Some message. Also contains M_FORBIDDEN, but won't be parsed")
|
||||
assertThat(loginError(error)).isEqualTo(CommonStrings.error_unknown)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ interface LogoutUseCase {
|
|||
/**
|
||||
* Log out the current user and then perform any needed cleanup tasks.
|
||||
* @param ignoreSdkError if true, the SDK error will be ignored and the user will be logged out anyway.
|
||||
* @return the session id of the logged out user.
|
||||
* @return an optional URL. When the URL is there, it should be presented to the user after logout for
|
||||
* Relying Party (RP) initiated logout on their account page.
|
||||
*/
|
||||
suspend fun logout(ignoreSdkError: Boolean): String
|
||||
suspend fun logout(ignoreSdkError: Boolean): String?
|
||||
|
||||
interface Factory {
|
||||
fun create(sessionId: String): LogoutUseCase
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.api.util
|
||||
|
||||
import android.app.Activity
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import timber.log.Timber
|
||||
|
||||
fun onSuccessLogout(
|
||||
activity: Activity,
|
||||
darkTheme: Boolean,
|
||||
url: String?,
|
||||
) {
|
||||
Timber.d("Success logout with result url: $url")
|
||||
url?.let {
|
||||
activity.openUrlInChromeCustomTab(null, darkTheme, it)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,27 +17,25 @@
|
|||
package io.element.android.features.logout.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.logout.api.LogoutUseCase
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultLogoutUseCase @AssistedInject constructor(
|
||||
@Assisted private val sessionId: String,
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLogoutUseCase @Inject constructor(
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
) : LogoutUseCase {
|
||||
@ContributesBinding(AppScope::class)
|
||||
@AssistedFactory
|
||||
interface Factory : LogoutUseCase.Factory {
|
||||
override fun create(sessionId: String): DefaultLogoutUseCase
|
||||
}
|
||||
|
||||
override suspend fun logout(ignoreSdkError: Boolean): String {
|
||||
val matrixClient = matrixClientProvider.getOrRestore(SessionId(sessionId)).getOrThrow()
|
||||
matrixClient.logout(ignoreSdkError = ignoreSdkError)
|
||||
return sessionId
|
||||
override suspend fun logout(ignoreSdkError: Boolean): String? {
|
||||
val currentSession = authenticationService.getLatestSessionId()
|
||||
return if (currentSession != null) {
|
||||
matrixClientProvider.getOrRestore(currentSession)
|
||||
.getOrThrow()
|
||||
.logout(userInitiated = true, ignoreSdkError = true)
|
||||
} else {
|
||||
error("No session to sign out")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.logout.api.LogoutEntryPoint
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.features.logout.api.util.onSuccessLogout
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class LogoutNode @AssistedInject constructor(
|
||||
|
|
@ -42,21 +42,15 @@ class LogoutNode @AssistedInject constructor(
|
|||
plugins<LogoutEntryPoint.Callback>().forEach { it.onChangeRecoveryKeyClick() }
|
||||
}
|
||||
|
||||
private fun onSuccessLogout(activity: Activity, url: String?) {
|
||||
Timber.d("Success logout with result url: $url")
|
||||
url?.let {
|
||||
activity.openUrlInChromeCustomTab(null, false, it)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val activity = LocalContext.current as Activity
|
||||
val isDark = ElementTheme.isLightTheme.not()
|
||||
LogoutView(
|
||||
state = state,
|
||||
onChangeRecoveryKeyClick = ::onChangeRecoveryKeyClick,
|
||||
onSuccessLogout = { onSuccessLogout(activity, it) },
|
||||
onSuccessLogout = { onSuccessLogout(activity, isDark, it) },
|
||||
onBackClick = ::navigateUp,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ class LogoutPresenter @Inject constructor(
|
|||
ignoreSdkError: Boolean,
|
||||
) = launch {
|
||||
suspend {
|
||||
matrixClient.logout(ignoreSdkError)
|
||||
matrixClient.logout(userInitiated = true, ignoreSdkError)
|
||||
}.runCatchingUpdatingState(logoutAction)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.features.logout.api.LogoutUseCase
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||
|
||||
@Module
|
||||
@ContributesTo(SessionScope::class)
|
||||
object SessionLogoutModule {
|
||||
@Provides
|
||||
fun provideLogoutUseCase(
|
||||
currentSessionIdHolder: CurrentSessionIdHolder,
|
||||
factory: DefaultLogoutUseCase.Factory,
|
||||
): LogoutUseCase {
|
||||
return factory.create(currentSessionIdHolder.current.value)
|
||||
}
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ class DefaultDirectLogoutPresenter @Inject constructor(
|
|||
ignoreSdkError: Boolean,
|
||||
) = launch {
|
||||
suspend {
|
||||
matrixClient.logout(ignoreSdkError)
|
||||
matrixClient.logout(userInitiated = true, ignoreSdkError)
|
||||
}.runCatchingUpdatingState(logoutAction)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class LogoutPresenterTest {
|
|||
@Test
|
||||
fun `present - logout with error then cancel`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
logoutLambda = { _ ->
|
||||
logoutLambda = { _, _ ->
|
||||
throw A_THROWABLE
|
||||
}
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ class LogoutPresenterTest {
|
|||
@Test
|
||||
fun `present - logout with error then force`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
logoutLambda = { ignoreSdkError ->
|
||||
logoutLambda = { ignoreSdkError, _ ->
|
||||
if (!ignoreSdkError) {
|
||||
throw A_THROWABLE
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ class DefaultDirectLogoutPresenterTest {
|
|||
@Test
|
||||
fun `present - logout with error then cancel`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
logoutLambda = { _ ->
|
||||
logoutLambda = { _, _ ->
|
||||
throw A_THROWABLE
|
||||
}
|
||||
}
|
||||
|
|
@ -153,7 +153,7 @@ class DefaultDirectLogoutPresenterTest {
|
|||
@Test
|
||||
fun `present - logout with error then force`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
logoutLambda = { ignoreSdkError ->
|
||||
logoutLambda = { ignoreSdkError, _ ->
|
||||
if (!ignoreSdkError) {
|
||||
throw A_THROWABLE
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ import io.element.android.features.logout.api.LogoutUseCase
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeLogoutUseCase(
|
||||
var logoutLambda: (Boolean) -> String = lambdaError()
|
||||
var logoutLambda: (Boolean) -> String? = { lambdaError() }
|
||||
) : LogoutUseCase {
|
||||
override suspend fun logout(ignoreSdkError: Boolean): String {
|
||||
override suspend fun logout(ignoreSdkError: Boolean): String? {
|
||||
return logoutLambda(ignoreSdkError)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ package io.element.android.features.messages.impl
|
|||
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
|
||||
sealed interface MessagesEvents {
|
||||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
|
||||
data class ToggleReaction(val emoji: String, val eventId: EventId) : MessagesEvents
|
||||
data class ToggleReaction(val emoji: String, val uniqueId: UniqueId) : MessagesEvents
|
||||
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
|
||||
data object Dismiss : MessagesEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
|||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||
|
|
@ -194,7 +194,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
is MessagesEvents.ToggleReaction -> {
|
||||
localCoroutineScope.toggleReaction(event.emoji, event.eventId)
|
||||
localCoroutineScope.toggleReaction(event.emoji, event.uniqueId)
|
||||
}
|
||||
is MessagesEvents.InviteDialogDismissed -> {
|
||||
hasDismissedInviteDialog = true
|
||||
|
|
@ -316,10 +316,10 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
|
||||
private fun CoroutineScope.toggleReaction(
|
||||
emoji: String,
|
||||
eventId: EventId,
|
||||
uniqueId: UniqueId,
|
||||
) = launch(dispatchers.io) {
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
toggleReaction(emoji, eventId)
|
||||
toggleReaction(emoji, uniqueId)
|
||||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,8 +175,7 @@ fun MessagesView(
|
|||
}
|
||||
|
||||
fun onEmojiReactionClick(emoji: String, event: TimelineItem.Event) {
|
||||
if (event.eventId == null) return
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.eventId))
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.id))
|
||||
}
|
||||
|
||||
fun onEmojiReactionLongClick(emoji: String, event: TimelineItem.Event) {
|
||||
|
|
@ -248,7 +247,6 @@ fun MessagesView(
|
|||
state = state.actionListState,
|
||||
onSelectAction = ::onActionSelected,
|
||||
onCustomReactionClick = { event ->
|
||||
if (event.eventId == null) return@ActionListView
|
||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||
},
|
||||
onEmojiReactionClick = ::onEmojiReactionClick,
|
||||
|
|
@ -256,8 +254,8 @@ fun MessagesView(
|
|||
|
||||
CustomReactionBottomSheet(
|
||||
state = state.customReactionState,
|
||||
onSelectEmoji = { eventId, emoji ->
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
|
||||
onSelectEmoji = { uniqueId, emoji ->
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, uniqueId))
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -126,7 +126,6 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
|||
)
|
||||
|
||||
val displayEmojiReactions = usersEventPermissions.canSendReaction &&
|
||||
timelineItem.isRemote &&
|
||||
timelineItem.content.canReact()
|
||||
if (actions.isNotEmpty() || displayEmojiReactions) {
|
||||
target.value = ActionListState.Target.Success(
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import io.element.android.libraries.architecture.Presenter
|
|||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
|
|
@ -97,7 +98,7 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
||||
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION, updateKey = syncUpdateFlow.value)
|
||||
|
||||
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val prevMostRecentItemId = rememberSaveable { mutableStateOf<UniqueId?>(null) }
|
||||
|
||||
val newEventState = remember { mutableStateOf(NewEventState.None) }
|
||||
val messageShield: MutableState<MessageShield?> = remember { mutableStateOf(null) }
|
||||
|
|
@ -244,7 +245,7 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
*/
|
||||
private suspend fun computeNewItemState(
|
||||
timelineItems: ImmutableList<TimelineItem>,
|
||||
prevMostRecentItemId: MutableState<String?>,
|
||||
prevMostRecentItemId: MutableState<UniqueId?>,
|
||||
newEventState: MutableState<NewEventState>
|
||||
) = withContext(dispatchers.computation) {
|
||||
// FromMe is prioritized over FromOther, so skip if we already have a FromMe
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
|
|
@ -123,7 +124,10 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
|||
}
|
||||
|
||||
fun aTimelineItemDaySeparator(): TimelineItem.Virtual {
|
||||
return TimelineItem.Virtual(UUID.randomUUID().toString(), aTimelineItemDaySeparatorModel("Today"))
|
||||
return TimelineItem.Virtual(
|
||||
id = UniqueId(UUID.randomUUID().toString()),
|
||||
model = aTimelineItemDaySeparatorModel("Today"),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aTimelineItemEvent(
|
||||
|
|
@ -145,7 +149,7 @@ internal fun aTimelineItemEvent(
|
|||
messageShield: MessageShield? = null,
|
||||
): TimelineItem.Event {
|
||||
return TimelineItem.Event(
|
||||
id = UUID.randomUUID().toString(),
|
||||
id = UniqueId(UUID.randomUUID().toString()),
|
||||
eventId = eventId,
|
||||
transactionId = transactionId,
|
||||
senderId = UserId("@senderId:domain"),
|
||||
|
|
@ -211,7 +215,7 @@ internal fun aTimelineItemReadReceipts(
|
|||
}
|
||||
|
||||
internal fun aGroupedEvents(
|
||||
id: Long = 0,
|
||||
id: UniqueId = UniqueId("0"),
|
||||
withReadReceipts: Boolean = false,
|
||||
): TimelineItem.GroupedEvents {
|
||||
val event1 = aTimelineItemEvent(
|
||||
|
|
@ -232,7 +236,7 @@ internal fun aGroupedEvents(
|
|||
)
|
||||
val events = listOf(event1, event2)
|
||||
return TimelineItem.GroupedEvents(
|
||||
id = id.toString(),
|
||||
id = id,
|
||||
events = events.toImmutableList(),
|
||||
aggregatedReadReceipts = events.flatMap { it.readReceiptState.receipts }.toImmutableList(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ fun TimelineItemGroupedEventsRow(
|
|||
)
|
||||
},
|
||||
) {
|
||||
val isExpanded = rememberSaveable(key = timelineItem.identifier()) { mutableStateOf(false) }
|
||||
val isExpanded = rememberSaveable(key = timelineItem.identifier().value) { mutableStateOf(false) }
|
||||
|
||||
fun onExpandGroupClick() {
|
||||
isExpanded.value = !isExpanded.value
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@ import androidx.compose.ui.Modifier
|
|||
import io.element.android.emojibasebindings.Emoji
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.hide
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CustomReactionBottomSheet(
|
||||
state: CustomReactionState,
|
||||
onSelectEmoji: (EventId, Emoji) -> Unit,
|
||||
onSelectEmoji: (UniqueId, Emoji) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
|
|
@ -43,10 +43,10 @@ fun CustomReactionBottomSheet(
|
|||
}
|
||||
|
||||
fun onEmojiSelectedDismiss(emoji: Emoji) {
|
||||
if (target?.event?.eventId == null) return
|
||||
if (target?.event == null) return
|
||||
sheetState.hide(coroutineScope) {
|
||||
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
onSelectEmoji(target.event.eventId, emoji)
|
||||
onSelectEmoji(target.event.id, emoji)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.annotation.VisibleForTesting
|
|||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -71,7 +72,7 @@ private fun MutableList<TimelineItem>.addGroup(
|
|||
val groupId = groupIds.getOrPutGroupId(groupOfItems)
|
||||
add(
|
||||
TimelineItem.GroupedEvents(
|
||||
id = groupId,
|
||||
id = UniqueId(groupId),
|
||||
events = groupOfItems.toImmutableList(),
|
||||
aggregatedReadReceipts = groupOfItems.flatMap { it.readReceiptState.receipts }.toImmutableList()
|
||||
)
|
||||
|
|
@ -83,15 +84,15 @@ private fun MutableMap<String, String>.getOrPutGroupId(timelineItems: List<Timel
|
|||
assert(timelineItems.isNotEmpty())
|
||||
for (item in timelineItems) {
|
||||
val itemIdentifier = item.identifier()
|
||||
if (this.contains(itemIdentifier)) {
|
||||
return this[itemIdentifier]!!
|
||||
if (this.contains(itemIdentifier.value)) {
|
||||
return this[itemIdentifier.value]!!
|
||||
}
|
||||
}
|
||||
val timelineItem = timelineItems.first()
|
||||
return computeGroupIdWith(timelineItem).also { groupId ->
|
||||
this[timelineItem.identifier()] = groupId
|
||||
return computeGroupIdWith(timelineItem).value.also { groupId ->
|
||||
this[timelineItem.identifier().value] = groupId
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun computeGroupIdWith(timelineItem: TimelineItem): String = "${timelineItem.identifier()}_group"
|
||||
internal fun computeGroupIdWith(timelineItem: TimelineItem): UniqueId = UniqueId("${timelineItem.identifier()}_group")
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
|
|
@ -36,7 +37,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
|
||||
@Immutable
|
||||
sealed interface TimelineItem {
|
||||
fun identifier(): String = when (this) {
|
||||
fun identifier(): UniqueId = when (this) {
|
||||
is Event -> id
|
||||
is Virtual -> id
|
||||
is GroupedEvents -> id
|
||||
|
|
@ -58,13 +59,13 @@ sealed interface TimelineItem {
|
|||
|
||||
@Immutable
|
||||
data class Virtual(
|
||||
val id: String,
|
||||
val id: UniqueId,
|
||||
val model: TimelineItemVirtualModel
|
||||
) : TimelineItem
|
||||
|
||||
@Immutable
|
||||
data class Event(
|
||||
val id: String,
|
||||
val id: UniqueId,
|
||||
// Note: eventId can be null when the event is a local echo
|
||||
val eventId: EventId? = null,
|
||||
val transactionId: TransactionId? = null,
|
||||
|
|
@ -101,7 +102,7 @@ sealed interface TimelineItem {
|
|||
|
||||
@Immutable
|
||||
data class GroupedEvents(
|
||||
val id: String,
|
||||
val id: UniqueId,
|
||||
val events: ImmutableList<Event>,
|
||||
val aggregatedReadReceipts: ImmutableList<ReadReceiptData>,
|
||||
) : TimelineItem
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
|||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
|
|
@ -81,11 +82,11 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
|||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
|
|
@ -201,8 +202,8 @@ class MessagesPresenterTest {
|
|||
@Test
|
||||
fun `present - handle toggling a reaction`() = runTest {
|
||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
val toggleReactionSuccess = lambdaRecorder { _: String, _: EventId -> Result.success(Unit) }
|
||||
val toggleReactionFailure = lambdaRecorder { _: String, _: EventId -> Result.failure<Unit>(IllegalStateException("Failed to send reaction")) }
|
||||
val toggleReactionSuccess = lambdaRecorder { _: String, _: UniqueId -> Result.success(Unit) }
|
||||
val toggleReactionFailure = lambdaRecorder { _: String, _: UniqueId -> Result.failure<Unit>(IllegalStateException("Failed to send reaction")) }
|
||||
|
||||
val timeline = FakeTimeline().apply {
|
||||
this.toggleReactionLambda = toggleReactionSuccess
|
||||
|
|
@ -222,25 +223,25 @@ class MessagesPresenterTest {
|
|||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
|
||||
// No crashes when sending a reaction failed
|
||||
timeline.apply { toggleReactionLambda = toggleReactionFailure }
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
|
||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||
|
||||
assert(toggleReactionSuccess)
|
||||
.isCalledOnce()
|
||||
.with(value("👍"), value(AN_EVENT_ID))
|
||||
.with(value("👍"), value(A_UNIQUE_ID))
|
||||
assert(toggleReactionFailure)
|
||||
.isCalledOnce()
|
||||
.with(value("👍"), value(AN_EVENT_ID))
|
||||
.with(value("👍"), value(A_UNIQUE_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle toggling a reaction twice`() = runTest {
|
||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
val toggleReactionSuccess = lambdaRecorder { _: String, _: EventId -> Result.success(Unit) }
|
||||
val toggleReactionSuccess = lambdaRecorder { _: String, _: UniqueId -> Result.success(Unit) }
|
||||
|
||||
val timeline = FakeTimeline().apply {
|
||||
this.toggleReactionLambda = toggleReactionSuccess
|
||||
|
|
@ -259,13 +260,13 @@ class MessagesPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
|
||||
assert(toggleReactionSuccess)
|
||||
.isCalledExactly(2)
|
||||
.withSequence(
|
||||
listOf(value("👍"), value(AN_EVENT_ID)),
|
||||
listOf(value("👍"), value(AN_EVENT_ID)),
|
||||
listOf(value("👍"), value(A_UNIQUE_ID)),
|
||||
listOf(value("👍"), value(A_UNIQUE_ID)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ class MessagesViewTest {
|
|||
state = state,
|
||||
)
|
||||
rule.onAllNodesWithText("👍️").onFirst().performClick()
|
||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction("👍️", timelineItem.eventId!!))
|
||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction("👍️", timelineItem.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -463,7 +463,7 @@ class MessagesViewTest {
|
|||
// Give time for the close animation to complete
|
||||
rule.mainClock.advanceTimeBy(milliseconds = 1_000)
|
||||
customReactionStateEventsRecorder.assertSingle(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction(aUnicode, timelineItem.eventId!!))
|
||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction(aUnicode, timelineItem.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -758,7 +758,7 @@ class ActionListPresenterTest {
|
|||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
event = messageEvent,
|
||||
displayEmojiReactions = false,
|
||||
displayEmojiReactions = true,
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.Copy,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||
|
|
@ -51,7 +52,7 @@ internal fun aMessageEvent(
|
|||
sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID),
|
||||
messageShield: MessageShield? = null,
|
||||
) = TimelineItem.Event(
|
||||
id = eventId?.value.orEmpty(),
|
||||
id = UniqueId(eventId?.value.orEmpty()),
|
||||
eventId = eventId,
|
||||
transactionId = transactionId,
|
||||
senderId = A_USER_ID,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
|
|
@ -87,7 +89,7 @@ class PinnedMessagesBannerPresenterTest {
|
|||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = "FAKE_UNIQUE_ID",
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID,
|
||||
content = messageContent,
|
||||
|
|
@ -119,14 +121,14 @@ class PinnedMessagesBannerPresenterTest {
|
|||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = "FAKE_UNIQUE_ID",
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID,
|
||||
content = messageContent1,
|
||||
),
|
||||
),
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = "FAKE_UNIQUE_ID_2",
|
||||
uniqueId = A_UNIQUE_ID_2,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID_2,
|
||||
content = messageContent2,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ class TimelineItemIndexerTest {
|
|||
groupedEvents.events.forEach { eventIds.add(it.eventId!!) }
|
||||
},
|
||||
TimelineItem.Virtual(
|
||||
id = "dummy",
|
||||
id = UniqueId("dummy"),
|
||||
model = TimelineItemReadMarkerModel
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ 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.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
|
|
@ -46,6 +47,8 @@ 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.AN_EVENT_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
|
|
@ -79,9 +82,6 @@ import java.util.Date
|
|||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
||||
private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class) class TimelinePresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
|
@ -131,7 +131,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
val timeline = FakeTimeline(
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -164,9 +164,9 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
val timeline = FakeTimeline(
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID_2,
|
||||
uniqueId = A_UNIQUE_ID_2,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID_2,
|
||||
content = aMessageContent("Test message")
|
||||
|
|
@ -201,9 +201,9 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
val timeline = FakeTimeline(
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID_2,
|
||||
uniqueId = A_UNIQUE_ID_2,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID_2,
|
||||
content = aMessageContent("Test message")
|
||||
|
|
@ -243,9 +243,9 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
val timeline = FakeTimeline(
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID_2,
|
||||
uniqueId = A_UNIQUE_ID_2,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID_2,
|
||||
content = aMessageContent("Test message")
|
||||
|
|
@ -279,8 +279,8 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
val timeline = FakeTimeline(
|
||||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker),
|
||||
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
|
||||
MatrixTimelineItem.Virtual(A_UNIQUE_ID, VirtualTimelineItem.ReadMarker),
|
||||
MatrixTimelineItem.Virtual(A_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
|
||||
)
|
||||
)
|
||||
).apply {
|
||||
|
|
@ -310,13 +310,13 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
assertThat(initialState.newEventState).isEqualTo(NewEventState.None)
|
||||
assertThat(initialState.timelineItems.size).isEqualTo(0)
|
||||
timelineItems.emit(
|
||||
listOf(MatrixTimelineItem.Event("0", anEventTimelineItem(content = aMessageContent())))
|
||||
listOf(MatrixTimelineItem.Event(UniqueId("0"), anEventTimelineItem(content = aMessageContent())))
|
||||
)
|
||||
consumeItemsUntilPredicate { it.timelineItems.size == 1 }
|
||||
// Mimics sending a message, and assert newEventState is FromMe
|
||||
timelineItems.getAndUpdate { items ->
|
||||
val event = anEventTimelineItem(content = aMessageContent(), isOwn = true)
|
||||
items + listOf(MatrixTimelineItem.Event("1", event))
|
||||
items + listOf(MatrixTimelineItem.Event(UniqueId("1"), event))
|
||||
}
|
||||
consumeItemsUntilPredicate { it.timelineItems.size == 2 }
|
||||
awaitLastSequentialItem().also { state ->
|
||||
|
|
@ -325,7 +325,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
// Mimics receiving a message without clearing the previous FromMe
|
||||
timelineItems.getAndUpdate { items ->
|
||||
val event = anEventTimelineItem(content = aMessageContent())
|
||||
items + listOf(MatrixTimelineItem.Event("2", event))
|
||||
items + listOf(MatrixTimelineItem.Event(UniqueId("2"), event))
|
||||
}
|
||||
consumeItemsUntilPredicate { it.timelineItems.size == 3 }
|
||||
|
||||
|
|
@ -337,7 +337,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
// Mimics receiving a message and assert newEventState is FromOther
|
||||
timelineItems.getAndUpdate { items ->
|
||||
val event = anEventTimelineItem(content = aMessageContent())
|
||||
items + listOf(MatrixTimelineItem.Event("3", event))
|
||||
items + listOf(MatrixTimelineItem.Event(UniqueId("3"), event))
|
||||
}
|
||||
consumeItemsUntilPredicate { it.timelineItems.size == 4 }
|
||||
awaitLastSequentialItem().also { state ->
|
||||
|
|
@ -381,7 +381,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
),
|
||||
)
|
||||
timelineItems.emit(
|
||||
listOf(MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(reactions = oneReaction)))
|
||||
listOf(MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(reactions = oneReaction)))
|
||||
)
|
||||
val item = awaitItem().timelineItems.first()
|
||||
assertThat(item).isInstanceOf(TimelineItem.Event::class.java)
|
||||
|
|
@ -476,7 +476,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID,
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(),
|
||||
)
|
||||
)
|
||||
|
|
@ -532,7 +532,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID,
|
||||
uniqueId = A_UNIQUE_ID,
|
||||
event = anEventTimelineItem(eventId = AN_EVENT_ID),
|
||||
)
|
||||
)
|
||||
|
|
@ -618,7 +618,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
|||
timelineItems = flowOf(
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
FAKE_UNIQUE_ID,
|
||||
A_UNIQUE_ID,
|
||||
anEventTimelineItem(
|
||||
sender = A_USER_ID,
|
||||
receipts = persistentListOf(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
|
|||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
|
||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||
|
|
@ -54,7 +55,7 @@ class TimelineViewTest {
|
|||
state = aTimelineState(
|
||||
timelineItems = persistentListOf<TimelineItem>(
|
||||
TimelineItem.Virtual(
|
||||
id = "backward_pagination",
|
||||
id = UniqueId("backward_pagination"),
|
||||
model = TimelineItemLoadingIndicatorModel(Timeline.PaginationDirection.BACKWARDS, 0)
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemRead
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
|
|
@ -37,7 +38,7 @@ class TimelineItemGrouperTest {
|
|||
private val sut = TimelineItemGrouper()
|
||||
|
||||
private val aGroupableItem = TimelineItem.Event(
|
||||
id = "0",
|
||||
id = UniqueId("0"),
|
||||
senderId = A_USER_ID,
|
||||
senderAvatar = anAvatarData(),
|
||||
senderProfile = aProfileTimelineDetailsReady(displayName = ""),
|
||||
|
|
@ -54,7 +55,7 @@ class TimelineItemGrouperTest {
|
|||
messageShield = null,
|
||||
)
|
||||
private val aNonGroupableItem = aMessageEvent()
|
||||
private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today"))
|
||||
private val aNonGroupableItemNoEvent = TimelineItem.Virtual(UniqueId("virtual"), aTimelineItemDaySeparatorModel("Today"))
|
||||
|
||||
@Test
|
||||
fun `test empty`() {
|
||||
|
|
@ -82,8 +83,8 @@ class TimelineItemGrouperTest {
|
|||
fun `test groupables and ensure reordering`() {
|
||||
val result = sut.group(
|
||||
listOf(
|
||||
aGroupableItem.copy(id = "1"),
|
||||
aGroupableItem.copy(id = "0"),
|
||||
aGroupableItem.copy(id = UniqueId("1")),
|
||||
aGroupableItem.copy(id = UniqueId("0")),
|
||||
),
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
|
|
@ -91,8 +92,8 @@ class TimelineItemGrouperTest {
|
|||
TimelineItem.GroupedEvents(
|
||||
id = computeGroupIdWith(aGroupableItem),
|
||||
events = listOf(
|
||||
aGroupableItem.copy("0"),
|
||||
aGroupableItem.copy(id = "1"),
|
||||
aGroupableItem.copy(id = UniqueId("0")),
|
||||
aGroupableItem.copy(id = UniqueId("1")),
|
||||
).toImmutableList(),
|
||||
aggregatedReadReceipts = emptyList<ReadReceiptData>().toImmutableList(),
|
||||
),
|
||||
|
|
@ -161,13 +162,13 @@ class TimelineItemGrouperTest {
|
|||
fun `when calling multiple time the method group over a growing list of groupable items, then groupId is stable`() {
|
||||
// When
|
||||
val groupableItems = mutableListOf(
|
||||
aGroupableItem.copy(id = "1"),
|
||||
aGroupableItem.copy(id = "2")
|
||||
aGroupableItem.copy(id = UniqueId("1")),
|
||||
aGroupableItem.copy(id = UniqueId("2"))
|
||||
)
|
||||
val expectedGroupId = sut.group(groupableItems).first().identifier()
|
||||
groupableItems.add(0, aGroupableItem.copy("3"))
|
||||
groupableItems.add(2, aGroupableItem.copy("4"))
|
||||
groupableItems.add(aGroupableItem.copy("5"))
|
||||
groupableItems.add(0, aGroupableItem.copy(UniqueId("3")))
|
||||
groupableItems.add(2, aGroupableItem.copy(UniqueId("4")))
|
||||
groupableItems.add(aGroupableItem.copy(UniqueId("5")))
|
||||
val actualGroupId = sut.group(groupableItems).first().identifier()
|
||||
// Then
|
||||
assertThat(actualGroupId).isEqualTo(expectedGroupId)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.voicemessages.timeline
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
|
|
@ -80,7 +81,7 @@ fun TestScope.aDefaultRedactedVoiceMessageManager(
|
|||
|
||||
fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = "0",
|
||||
uniqueId = UniqueId("0"),
|
||||
event = EventTimelineItem(
|
||||
eventId = eventId,
|
||||
transactionId = null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* 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.migration.impl.migrations
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesMultibinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.CacheDirectory
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Create the cache directory for the existing sessions.
|
||||
*/
|
||||
@ContributesMultibinding(AppScope::class)
|
||||
class AppMigration06 @Inject constructor(
|
||||
private val sessionStore: SessionStore,
|
||||
@CacheDirectory private val cacheDirectory: File,
|
||||
) : AppMigration {
|
||||
override val order: Int = 6
|
||||
|
||||
override suspend fun migrate() {
|
||||
val allSessions = sessionStore.getAllSessions()
|
||||
for (session in allSessions) {
|
||||
if (session.cachePath.isEmpty()) {
|
||||
val sessionFile = File(session.sessionPath)
|
||||
val sessionFolder = sessionFile.name
|
||||
val cachePath = File(cacheDirectory, sessionFolder)
|
||||
sessionStore.updateData(session.copy(cachePath = cachePath.absolutePath))
|
||||
// Move existing cache files
|
||||
listOf(
|
||||
"matrix-sdk-event-cache.sqlite3",
|
||||
"matrix-sdk-event-cache.sqlite3-shm",
|
||||
"matrix-sdk-event-cache.sqlite3-wal",
|
||||
).map { fileName ->
|
||||
File(sessionFile, fileName)
|
||||
}.takeIf { files ->
|
||||
files.all { it.exists() }
|
||||
}?.forEach { cacheFile ->
|
||||
val targetFile = File(cachePath, cacheFile.name)
|
||||
cacheFile.copyTo(targetFile)
|
||||
cacheFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.poll.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
|
|
@ -32,8 +33,8 @@ fun aPollTimelineItems(
|
|||
return flowOf(
|
||||
polls.map { entry ->
|
||||
MatrixTimelineItem.Event(
|
||||
entry.key.value,
|
||||
anEventTimelineItem(
|
||||
uniqueId = UniqueId(entry.key.value),
|
||||
event = anEventTimelineItem(
|
||||
eventId = entry.key,
|
||||
content = entry.value,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ dependencies {
|
|||
implementation(projects.features.lockscreen.api)
|
||||
implementation(projects.features.analytics.api)
|
||||
implementation(projects.features.ftue.api)
|
||||
implementation(projects.features.licenses.api)
|
||||
implementation(projects.features.logout.api)
|
||||
implementation(projects.features.roomlist.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.features.logout.api.LogoutEntryPoint
|
||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||
|
|
@ -59,6 +60,7 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||
private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint,
|
||||
private val logoutEntryPoint: LogoutEntryPoint,
|
||||
private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint,
|
||||
) : BaseFlowNode<PreferencesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(),
|
||||
|
|
@ -106,6 +108,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data object SignOut : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object OssLicenses : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -170,7 +175,12 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
createNode<ConfigureTracingNode>(buildContext)
|
||||
}
|
||||
NavTarget.About -> {
|
||||
createNode<AboutNode>(buildContext)
|
||||
val callback = object : AboutNode.Callback {
|
||||
override fun openOssLicenses() {
|
||||
backstack.push(NavTarget.OssLicenses)
|
||||
}
|
||||
}
|
||||
createNode<AboutNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.AnalyticsSettings -> {
|
||||
createNode<AnalyticsSettingsNode>(buildContext)
|
||||
|
|
@ -232,6 +242,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
.callback(callBack)
|
||||
.build()
|
||||
}
|
||||
is NavTarget.OssLicenses -> {
|
||||
openSourceLicensesEntryPoint.getNode(this, buildContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
|
|
@ -36,8 +35,11 @@ class AboutNode @AssistedInject constructor(
|
|||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: AboutPresenter,
|
||||
private val openSourceLicensesProvider: OpenSourceLicensesProvider,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun openOssLicenses()
|
||||
}
|
||||
|
||||
private fun onElementLegalClick(
|
||||
activity: Activity,
|
||||
darkTheme: Boolean,
|
||||
|
|
@ -58,7 +60,7 @@ class AboutNode @AssistedInject constructor(
|
|||
onElementLegalClick(activity, isDark, elementLegal)
|
||||
},
|
||||
onOpenSourceLicensesClick = {
|
||||
openSourceLicensesProvider.navigateToOpenSourceLicenses(activity)
|
||||
plugins.filterIsInstance<Callback>().forEach { it.openOssLicenses() }
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,18 +17,14 @@
|
|||
package io.element.android.features.preferences.impl.about
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import javax.inject.Inject
|
||||
|
||||
class AboutPresenter @Inject constructor(
|
||||
private val openSourceLicensesProvider: OpenSourceLicensesProvider,
|
||||
) : Presenter<AboutState> {
|
||||
class AboutPresenter @Inject constructor() : Presenter<AboutState> {
|
||||
@Composable
|
||||
override fun present(): AboutState {
|
||||
return AboutState(
|
||||
elementLegals = getAllLegals(),
|
||||
hasOpenSourcesLicenses = openSourceLicensesProvider.hasOpenSourceLicenses,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,4 @@ package io.element.android.features.preferences.impl.about
|
|||
|
||||
data class AboutState(
|
||||
val elementLegals: List<ElementLegal>,
|
||||
val hasOpenSourcesLicenses: Boolean,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,13 +22,11 @@ open class AboutStateProvider : PreviewParameterProvider<AboutState> {
|
|||
override val values: Sequence<AboutState>
|
||||
get() = sequenceOf(
|
||||
anAboutState(),
|
||||
anAboutState(hasOpenSourcesLicenses = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun anAboutState(
|
||||
hasOpenSourcesLicenses: Boolean = false,
|
||||
elementLegals: List<ElementLegal> = getAllLegals(),
|
||||
) = AboutState(
|
||||
elementLegals = getAllLegals(),
|
||||
hasOpenSourcesLicenses = hasOpenSourcesLicenses,
|
||||
elementLegals = elementLegals,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,12 +45,10 @@ fun AboutView(
|
|||
onClick = { onElementLegalClick(elementLegal) }
|
||||
)
|
||||
}
|
||||
if (state.hasOpenSourcesLicenses) {
|
||||
PreferenceText(
|
||||
title = stringResource(id = CommonStrings.common_open_source_licenses),
|
||||
onClick = onOpenSourceLicensesClick,
|
||||
)
|
||||
}
|
||||
PreferenceText(
|
||||
title = stringResource(id = CommonStrings.common_open_source_licenses),
|
||||
onClick = onOpenSourceLicensesClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ import io.element.android.anvilannotations.ContributesNode
|
|||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutView
|
||||
import io.element.android.features.logout.api.util.onSuccessLogout
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class PreferencesRootNode @AssistedInject constructor(
|
||||
|
|
@ -94,13 +94,6 @@ class PreferencesRootNode @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onSuccessLogout(activity: Activity, url: String?) {
|
||||
Timber.d("Success (direct) logout with result url: $url")
|
||||
url?.let {
|
||||
activity.openUrlInChromeCustomTab(null, false, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOpenNotificationSettings() {
|
||||
plugins<Callback>().forEach { it.onOpenNotificationSettings() }
|
||||
}
|
||||
|
|
@ -153,7 +146,7 @@ class PreferencesRootNode @AssistedInject constructor(
|
|||
directLogoutView.Render(
|
||||
state = state.directLogoutState,
|
||||
onSuccessLogout = {
|
||||
onSuccessLogout(activity, it)
|
||||
onSuccessLogout(activity, isDark, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ Wenn du fortfährst, können sich einige deiner Einstellungen ändern."</string>
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
|
||||
<string name="screen_notification_settings_edit_mode_all_messages">"Alle Nachrichten"</string>
|
||||
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string>
|
||||
<string name="screen_notification_settings_edit_screen_direct_section_header">"Benachrichtige mich bei Direktnachrichten über "</string>
|
||||
<string name="screen_notification_settings_edit_screen_direct_section_header">"Benachrichtige mich bei Direktnachrichten über"</string>
|
||||
<string name="screen_notification_settings_edit_screen_group_section_header">"Bei Gruppenchats benachrichtige mich bei"</string>
|
||||
<string name="screen_notification_settings_enable_notifications">"Benachrichtigungen auf diesem Gerät aktivieren"</string>
|
||||
<string name="screen_notification_settings_failed_fixing_configuration">"Die Konfiguration wurde nicht korrigiert, bitte versuche es erneut."</string>
|
||||
|
|
|
|||
|
|
@ -31,25 +31,12 @@ class AboutPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = AboutPresenter(FakeOpenSourceLicensesProvider(hasOpenSourceLicenses = true))
|
||||
val presenter = AboutPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.elementLegals).isEqualTo(getAllLegals())
|
||||
assertThat(initialState.hasOpenSourcesLicenses).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state, no open source licenses`() = runTest {
|
||||
val presenter = AboutPresenter(FakeOpenSourceLicensesProvider(hasOpenSourceLicenses = false))
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.elementLegals).isEqualTo(getAllLegals())
|
||||
assertThat(initialState.hasOpenSourcesLicenses).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package io.element.android.features.preferences.impl.about
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
|
|
@ -61,21 +60,10 @@ class AboutViewTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `if open source licenses are not available, the entry is not displayed`() {
|
||||
rule.setAboutView(
|
||||
anAboutState(),
|
||||
)
|
||||
val text = rule.activity.getString(CommonStrings.common_open_source_licenses)
|
||||
rule.onNodeWithText(text).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if open source licenses are available, clicking on the entry invokes the expected callback`() {
|
||||
fun `clicking on the open source licenses invokes the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setAboutView(
|
||||
anAboutState(
|
||||
hasOpenSourcesLicenses = true,
|
||||
),
|
||||
anAboutState(),
|
||||
onOpenSourceLicensesClick = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_open_source_licenses)
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ class DeveloperSettingsPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - toggling simplified sliding sync changes the preferences and logs out the user`() = runTest {
|
||||
val logoutCallRecorder = lambdaRecorder<Boolean, String> { "" }
|
||||
val logoutCallRecorder = lambdaRecorder<Boolean, String?> { "" }
|
||||
val logoutUseCase = FakeLogoutUseCase(logoutLambda = logoutCallRecorder)
|
||||
val preferences = InMemoryAppPreferencesStore()
|
||||
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences, logoutUseCase = logoutUseCase)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<string name="screen_bug_report_editor_supporting">"Iloji bo\'lsa, tavsifni ingliz tilida yozing."</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Buzilish jurnallarini yuboring"</string>
|
||||
<string name="screen_bug_report_include_logs">"Jurnallarga ruxsat bering"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Ekran tasvirini yuboring "</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Ekran tasvirini yuboring"</string>
|
||||
<string name="screen_bug_report_logs_description">"Har bir narsa to\'ri ishlayotganiga ishonch hosil qilish uchun xabaringizga jurnallar kiritiladi. Xabarni jurnallarsiz yuborish uchun ushbu sozlamani oʻchiring."</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$soxirgi marta ishlatilganda qulab tushdi. Biz bilan nosozlik hisobotini baham ko\'rmoqchimisiz?"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@ class DefaultBugReporterTest {
|
|||
slidingSyncProxy = null,
|
||||
passphrase = null,
|
||||
sessionPath = "session",
|
||||
cachePath = "cache",
|
||||
)
|
||||
@Test
|
||||
fun `test sendBugReport error`() = runTest {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<string name="screen_room_details_error_muting">"Bu xona ovozini o‘chirib bo‘lmadi, qayta urinib ko‘ring."</string>
|
||||
<string name="screen_room_details_error_unmuting">"Bu xonaning ovozi yoqilmadi, qayta urinib ko‘ring."</string>
|
||||
<string name="screen_room_details_invite_people_title">"Odamlarni taklif qiling"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Xonani tark etish "</string>
|
||||
<string name="screen_room_details_leave_room_title">"Xonani tark etish"</string>
|
||||
<string name="screen_room_details_notification_mode_custom">"Maxsus"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"Standart"</string>
|
||||
<string name="screen_room_details_notification_title">"Bildirishnomalar"</string>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ interface RoomListEntryPoint : FeatureEntryPoint {
|
|||
fun onRoomClick(roomId: RoomId)
|
||||
fun onCreateRoomClick()
|
||||
fun onSettingsClick()
|
||||
fun onSetUpRecoveryClick()
|
||||
fun onSessionConfirmRecoveryKeyClick()
|
||||
fun onRoomSettingsClick(roomId: RoomId)
|
||||
fun onReportBugClick()
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ class RoomListNode @AssistedInject constructor(
|
|||
plugins<RoomListEntryPoint.Callback>().forEach { it.onCreateRoomClick() }
|
||||
}
|
||||
|
||||
private fun onSetUpRecoveryClick() {
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onSetUpRecoveryClick() }
|
||||
}
|
||||
|
||||
private fun onSessionConfirmRecoveryKeyClick() {
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionConfirmRecoveryKeyClick() }
|
||||
}
|
||||
|
|
@ -98,6 +102,7 @@ class RoomListNode @AssistedInject constructor(
|
|||
onRoomClick = this::onRoomClick,
|
||||
onSettingsClick = this::onOpenSettings,
|
||||
onCreateRoomClick = this::onCreateRoomClick,
|
||||
onSetUpRecoveryClick = this::onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = this::onSessionConfirmRecoveryKeyClick,
|
||||
onRoomSettingsClick = this::onRoomSettingsClick,
|
||||
onMenuActionClick = { onMenuActionClick(activity, it) },
|
||||
|
|
|
|||
|
|
@ -187,8 +187,15 @@ class RoomListPresenter @Inject constructor(
|
|||
derivedStateOf {
|
||||
when {
|
||||
currentSecurityBannerDismissed -> SecurityBannerState.None
|
||||
recoveryState == RecoveryState.INCOMPLETE &&
|
||||
syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation
|
||||
syncState == SyncState.Running -> {
|
||||
when (recoveryState) {
|
||||
RecoveryState.UNKNOWN,
|
||||
RecoveryState.DISABLED -> SecurityBannerState.SetUpRecovery
|
||||
RecoveryState.INCOMPLETE -> SecurityBannerState.RecoveryKeyConfirmation
|
||||
RecoveryState.WAITING_FOR_SYNC,
|
||||
RecoveryState.ENABLED -> SecurityBannerState.None
|
||||
}
|
||||
}
|
||||
else -> SecurityBannerState.None
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ enum class InvitesState {
|
|||
|
||||
enum class SecurityBannerState {
|
||||
None,
|
||||
SetUpRecovery,
|
||||
RecoveryKeyConfirmation,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
|||
aRoomListState(contentState = aSkeletonContentState()),
|
||||
aRoomListState(matrixUser = MatrixUser(userId = UserId("@id:domain")), contentState = aMigrationContentState()),
|
||||
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ fun RoomListView(
|
|||
state: RoomListState,
|
||||
onRoomClick: (RoomId) -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onCreateRoomClick: () -> Unit,
|
||||
onRoomSettingsClick: (roomId: RoomId) -> Unit,
|
||||
|
|
@ -78,6 +79,7 @@ fun RoomListView(
|
|||
|
||||
RoomListScaffold(
|
||||
state = state,
|
||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||
onRoomClick = onRoomClick,
|
||||
onOpenSettings = onSettingsClick,
|
||||
|
|
@ -106,6 +108,7 @@ fun RoomListView(
|
|||
@Composable
|
||||
private fun RoomListScaffold(
|
||||
state: RoomListState,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onRoomClick: (RoomId) -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
|
|
@ -142,6 +145,7 @@ private fun RoomListScaffold(
|
|||
contentState = state.contentState,
|
||||
filtersState = state.filtersState,
|
||||
eventSink = state.eventSink,
|
||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||
onRoomClick = ::onRoomClick,
|
||||
onCreateRoomClick = onCreateRoomClick,
|
||||
|
|
@ -178,6 +182,7 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class)
|
|||
state = state,
|
||||
onRoomClick = {},
|
||||
onSettingsClick = {},
|
||||
onSetUpRecoveryClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onCreateRoomClick = {},
|
||||
onRoomSettingsClick = {},
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ fun RoomListContentView(
|
|||
contentState: RoomListContentState,
|
||||
filtersState: RoomListFiltersState,
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onRoomClick: (RoomListRoomSummary) -> Unit,
|
||||
onCreateRoomClick: () -> Unit,
|
||||
|
|
@ -95,6 +96,7 @@ fun RoomListContentView(
|
|||
state = contentState,
|
||||
filtersState = filtersState,
|
||||
eventSink = eventSink,
|
||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||
onRoomClick = onRoomClick,
|
||||
)
|
||||
|
|
@ -141,6 +143,7 @@ private fun RoomsView(
|
|||
state: RoomListContentState.Rooms,
|
||||
filtersState: RoomListFiltersState,
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onRoomClick: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -154,6 +157,7 @@ private fun RoomsView(
|
|||
RoomsViewList(
|
||||
state = state,
|
||||
eventSink = eventSink,
|
||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||
onRoomClick = onRoomClick,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
|
|
@ -165,6 +169,7 @@ private fun RoomsView(
|
|||
private fun RoomsViewList(
|
||||
state: RoomListContentState.Rooms,
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onRoomClick: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -188,21 +193,27 @@ private fun RoomsViewList(
|
|||
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80
|
||||
contentPadding = PaddingValues(bottom = 80.dp)
|
||||
) {
|
||||
if (state.securityBannerState != SecurityBannerState.None) {
|
||||
when (state.securityBannerState) {
|
||||
SecurityBannerState.RecoveryKeyConfirmation -> {
|
||||
item {
|
||||
ConfirmRecoveryKeyBanner(
|
||||
onContinueClick = onConfirmRecoveryKeyClick,
|
||||
onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
|
||||
)
|
||||
}
|
||||
when (state.securityBannerState) {
|
||||
SecurityBannerState.SetUpRecovery -> {
|
||||
item {
|
||||
SetUpRecoveryKeyBanner(
|
||||
onContinueClick = onSetUpRecoveryClick,
|
||||
onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
} else if (state.fullScreenIntentPermissionsState.shouldDisplayBanner) {
|
||||
item {
|
||||
FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState)
|
||||
SecurityBannerState.RecoveryKeyConfirmation -> {
|
||||
item {
|
||||
ConfirmRecoveryKeyBanner(
|
||||
onContinueClick = onConfirmRecoveryKeyClick,
|
||||
onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
|
||||
)
|
||||
}
|
||||
}
|
||||
SecurityBannerState.None -> if (state.fullScreenIntentPermissionsState.shouldDisplayBanner) {
|
||||
item {
|
||||
FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -276,6 +287,7 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
|
|||
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }
|
||||
),
|
||||
eventSink = {},
|
||||
onSetUpRecoveryClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onRoomClick = {},
|
||||
onCreateRoomClick = {},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.roomlist.impl.R
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
||||
@Composable
|
||||
internal fun SetUpRecoveryKeyBanner(
|
||||
onContinueClick: () -> Unit,
|
||||
onDismissClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
DialogLikeBannerMolecule(
|
||||
modifier = modifier,
|
||||
title = stringResource(R.string.banner_set_up_recovery_title),
|
||||
content = stringResource(R.string.banner_set_up_recovery_content),
|
||||
onSubmitClick = onContinueClick,
|
||||
onDismissClick = onDismissClick,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SetUpRecoveryKeyBannerPreview() = ElementPreview {
|
||||
SetUpRecoveryKeyBanner(
|
||||
onContinueClick = {},
|
||||
onDismissClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_set_up_recovery_content">"Vygenerujte nový klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup ke svým zařízením."</string>
|
||||
<string name="banner_set_up_recovery_title">"Nastavení obnovy"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Vaše záloha chatu není aktuálně synchronizována. Abyste si zachovali přístup k záloze chatu, musíte potvrdit klíč pro obnovení."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Potvrďte klíč pro obnovení"</string>
|
||||
<string name="full_screen_intent_banner_message">"Abyste nikdy nezmeškali důležitý hovor, změňte nastavení tak, abyste povolili oznámení na celé obrazovce, když je telefon uzamčen."</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_set_up_recovery_content">"Loo uus taastevõti, mida saad kasutada oma krüptitud sõnumite ajaloo taastamisel olukorras, kus kaotad ligipääsu oma seadmetele."</string>
|
||||
<string name="banner_set_up_recovery_title">"Seadista taastamine"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Sinu vestluste varukoopia pole hetkel sünkroonis. Säilitamaks ligipääsu vestluse varukoopiale palun sisesta oma taastevõti."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Sisesta oma taastevõti"</string>
|
||||
<string name="full_screen_intent_banner_message">"Selleks, et sul ainsamgi tähtis kõne ei jääks märkamata, siis palun muuda oma nutiseadme seadistusi nii, et lukustusvaates oleksid täisekraani mõõtu teavitused."</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_set_up_recovery_content">"Hozzon létre egy új helyreállítási kulcsot, amellyel visszaállíthatja a titkosított üzenetek előzményeit, ha elveszíti az eszközökhöz való hozzáférést."</string>
|
||||
<string name="banner_set_up_recovery_title">"Helyreállítás beállítása"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"A csevegés biztonsági mentése nincs szinkronban. Meg kell erősítenie a helyreállítási kulcsát, hogy továbbra is hozzáférjen a csevegés biztonsági mentéséhez."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Helyreállítási kulcs megerősítése"</string>
|
||||
<string name="full_screen_intent_banner_message">"Annak érdekében, hogy soha ne maradjon le egyetlen fontos hívásról sem, módosítsa a beállításokat, hogy engedélyezze a teljes képernyős értesítéseket, amikor a telefon zárolva van."</string>
|
||||
|
|
|
|||
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